什么是类
C++是什么?C++设计之初就是class with c,所以简单点说,C++就是带类的C,那么什么是类?
类,简单点说就是类型,在C++中我们一开始所接触的类型有如下几种:
//+-------------------
char,
short,
int,
long,
long long,
float,
double
……
//+------------------
这些类型属于语言本身的一部分,我们称之为基本类型,基本类型不可被更改,也不可被创造,更不可被消灭,任何一个程序都是有基本类型搭建起来的,
比如,我们想要用一个类型来表示一个学生,那么我们可以char*,来表示他的名字,用unsigned int来表示他的学号,用double来表示他的成绩等等,
而这个表示学生信息的类型是由我们自定义而来,所以我们称之为自定义类型,在C语言里面,如果我们想要自定义一个类型出来,那么我们只能用关键字
struct或者union,常使用的是struct,而union仅仅用于某些特殊的场合,所以我们可以按如下的放下来定义一个自定义类型:
//+-----------------
typedef struct UserType{
int a;
double b;
long long c;
}* __LPUSERTYPE;
//+----------------
这个自定义类型由三个数据段组成,当然如果你要问我干嘛这么定义,我想应该必要的时候会这么定义吧。那么,既然说C++是带类的C,那么在C++里面扩展一个自定义类型又
该如何呢?当然,如果我说class在很多时候等同于struct的话那么这个问题是不是就不再是问题了呢?ok,如果刚才的那个UserType到底表示什么都不清楚的话,那么下面我们
尝试用一种能够说清楚的类型来阐述自定义类型的定义:
//+---------------
class Point{
public:
double x;
double y;
};
//+---------------
class : C++ 关键字,表示接下来要定义一个类型啦。
Point : 类型名,总是跟在class的后面,指明类型名是什么,class 和 类型名的中间还可以有其他的东西,比如我们在写com的时候使用的uuid,比如我们要导出一个类时候使用
的__declspec(dllexport)等。
{} : class 的代码段。
在 C++ 里面,class 是一句完整的C++语句,C++语句都是以";"结束,所以在"}"后面需要要用表示结束的";"号,否则你会遇到各种你预想不到的错误,当然,该语法对于C语言
的struct也同样实用。 那么class和struct又有什么区别呢?在C语言里面,struct里面所定义的数据类型都是可以直接访问的,简单点说C语言的struct的数据是共有的,同时C语言里的struct里面不可以
有成员函数,当然这个限制在C++中已经被摒弃,在C++中,struct和class的唯一区别就是默认权限的区别,在C语言中没有权限的概念,但C++作为面向对象的编程语言,所以自
然提供了权限的概念,以便于数据的封装,只是struct的默认权限是public,而class的默认权限是private,public顾名思义是公共的,private是私有的,当然除了public和private外还
存在一个权限:protected,private和protected所限制的数据都是外部不能够访问的,那么他们的区别是什么呢?private是纯粹的对数据进行封装,protected不但对数据进行封装,
还对继承留下一个后门。如你们所见,这里我们使用的plubic权限,public后面必须跟有":"号,所以在public下面的接口或者数据都是外部能够直接访问得到的。
那么在C++中,我们什么时候使用struct什么时候使用class呢?这里没有什么标准规范来限制,所以简单点说就是凡是使用struct的地方都可以使用class来替换,反之亦然,但是,
通常于C++来说有个不成文的规矩,那就是如果仅仅只是简单的定义一个组合类型的话我们使用struct,否则我们都应该使用class。
构造函数
什么是构造函数,从名字上面来理解,我们可以简单的认为就是构造对象的函数,一个类型想要被实例化,那么它首先调用的便是这个构造函数,而从代码的角度来理解的话构造
函数就是名字和类型名一样的函数,该函数可以有参数,但没有返回值,如果该函数没有参数,那么该函数被称为默认构造函数。
Point p;
这句代码直观上理解我们可能谁都清楚,但是现在我们想要知道,当我们定义一个Point的对象p的时候我们实际都经历了些什么?
第一,在栈上获取了一块内存。
第二,调用了Point的构造函数。
什么?调用了Point的构造函数?Point的构造函数是什么鬼?说好的和Point同样名字的函数怎么没看到呢?嗯,这就是我们要说的
默认构造函数,也就是说,当我们不给我们的类指定构造函数的时候编译器会为我们生成默认的构造函数,而这个函数什么,所以
虽然我们已经构造出一个Point的对象p出来,但是p里面的x和y是未初始化的,所以接下来我们需要针对xy进行各自的初始化,所以
无论出于什么样的理由,我们应该给Point添加相应的构造函数。
//+-----------------
class Point{
public:
Point(double __x = 0.0,double __y = 0.0):x(__x),y(__y){}
private:
double x;
double y;
};
//+-----------------
这次我们不但添加了构造函数,同时还将数据段放在private里面,我们将通过构造函数对数据进行初始化。该构造函数我们使用两个double类型作为参数,
并且两个参数都有默认值,所以我们下面的代码:
Point p;
将等同于:
Point p(0.0,0.0);
此时的x y的值分别都是0.0
:x(__x),y(__y)
在构造函数的括号后面有个冒号,冒号后面跟了一段代码,这段代码叫初始化列表,我们的xy便是在这里进行初始化的,我们使用第一个参数对x进行初始化,
使用第二个参数对y进行初始化。当然我们也可以不适用初始化列表:
//+------------------
class Point{
public:
Point(double __x = 0.0,double __y = 0.0){
x = __x;
y = __y;
}
private:
double x;
double y;
};
//+-------------------
这样的构造函数也是随处可见的,只是这样的写法和上面的写法有些不同(这不是废话吗?只要不瞎一看就是不同),哦!我这里说的不同是指效率上面的不同,
好吧,我们来剖析一下为什么不会不同,我们先来看看要实例化一个类我们所要经历的步骤:
第一,构造数据成员
第二,执行构造函数
所以,在执行构造函数之前xy已经被实例化出来了,所以当我们执行 x = __x 时又经历了一个复杂的过程,这个过程后面细说(但是由于我们此处使用的是基本类
型,所以这个过程也就被忽略啦,如果是自定义类型的话这期间又会有各种问题的产生),所以不管怎么说,我们应该优先选择使用初始化列表的方法来对数据进行初始化。
我们自定义一个类型目的就是为了使用他所封装的数据,但是像我们的Point类就是一个铁公鸡,就是说我们可以将数据放进去,但取不出来,嗯,这是一个问题,解决这个
问题的方法可以将数据段的private提升为public,这可以,但……
//+-----------------
void dealPoint(const Point& p) {
const_cast<Point&>(p).x = 1000;
}
int main()
{
Point p{ 100,200 };
dealPoint(p);
std::cout << p.x << std::endl;
std::cin.get();
return 0;
}
//+----------------
p的x的值直接被修改,当然有些时候我们可能想要这么干,但是这往往会带来意向不到的灾难性后果,因为你不知道什么时候哪根神经搭错了忽然间很想修改这个值。所以合理的做法应该是我们提供有方法直接访问内部需要访问的东西。
//+----------------
class Point{
public:
Point(double __x = 0.0,double __y = 0.0):x(__x),y(__y){}
double get_x() const{return x;}
double get_y() const{return y;}
void set_x(double __x){x = __x;}
void set_y(double __y){y = __y;}
void set(double __x,double __y){
x = __x;
y = __y;
}
private:
double x;
double y;
};
int main()
{
Point p(200, 300);
std::cout << p.get_x()<<"\t"<<p.get_y() << std::endl;
std::cin.get();
return 0;
}
//+----------------
复制构造函数
如果我们有一个Point,我们想要当前的Point去构造出一个相同的Point的时候我们应该怎么说呢?
Point p(200, 300);
Point p2(p);
就目前来说,如果我们写出这样的代码,编译通过是完全没问题的,同时运行也不会有任何问题。因为上面的第二句代码执行的并不是默认的构造函数,而是默认的复制构造函数,什么是复制构造函数呢?
复制构造函数就是函数名和类型一样,没有返回类型,而参数是该类型,如果我们不指定复制构造函数的话那么编译器会为我们的类升成默认的复制构造函数,所以上面的第二行代码执行的便是复制构造函数,最终是p == p2.
那么怎么编写复制构造函数呢?如下:
//+---------------
class Point{
public:
Point(double __x = 0.0,double __y = 0.0):x(__x),y(__y){}
Point(const Point& p):x(p.x),y(p.y){}
double get_x() const{return x;}
double get_y() const{return y;}
void set_x(double __x){x = __x;}
void set_y(double __y){y = __y;}
void set(double __x,double __y){
x = __x;
y = __y;
}
private:
double x;
double y;
};
//+------------------
赋值操作符
编译器会为class生成的不只有默认的构造函数和默认的复制构造函数,同时还会生成默认的赋值操作,正因为有这个默认的赋值操作符,所以我们下面的代码才会通过编译:
Point p(200,300); // 调用构造函数
Point p2 = p; // 调用复制构造函数
Point p3; // 调用默认的构造函数
p3 = p2 // 调用默认的赋值操作符
如果我们不使用编译器为我们准备的默认操作符的话,我们可以自己编写我们的赋值操作符,赋值操作符是这样的一个函数:
T& operator=(const T& other);
T 是我们的自定义类型。
所以如果我们自己编写赋值操作符,应该这样来:
//+-----------------
class Point{
public:
Point(double __x = 0.0,double __y = 0.0):x(__x),y(__y){}
Point(const Point& p):x(p.x),y(p.y){}
Point& operator=(const Point& other){
if(this == &other)
return *this;
x = other.x;
y = other.y;
return *this;
}
double get_x() const{return x;}
double get_y() const{return y;}
void set_x(double __x){x = __x;}
void set_y(double __y){y = __y;}
void set(double __x,double __y){
x = __x;
y = __y;
}
private:
double x;
double y;
};
//+---------------------
一个空类
//+-----------------
class Empty{};
//+----------------
当我们写下上面的类的时候,意味着我们写了些什么?
1,默认构造函数。
2,默认的复制构造函数
3,默认的赋值操作符
4,默认取地址操作符(该操作符的重载此处不做解释,熟悉之后自然也就明白了,所以该函数在一般的教科书中是不当作默认实现的函数,因为它本该存在)
5,析构函数
实际等同于:
//+-----------------
class Empty{
public:
Empty(){}
~Empty(){}
Empty(const Empty& other){}
Empty& operator=(const & Empty& other){return *this;}
};
//+-----------------
第一次我们引入析构函数,析构函数和构造函数相对应,构造函数初始化资源,所以析构函数的功能就是清理资源,那么什么时候需要我们自己实现构造函数呢?那就是当我们有资源需要我们手动释放的时候,比如堆上的指针,比如com对象的Release等等,如果说上面我们所举的Point例子其实是不需要复制操作符和复制构造函数的话(因为默认的就很好),那么我们现在来说一个我们必须要手赋值制操作符和复制构造函数的例子——字符串处理类,String。
在C++里面字符串有char*表示,但是纯粹的时候char*太过原始,一点都不对象,所以通常都会对char*进行封装,当然想要做一个完备的字符串类出来可不是一件简单的事,所以这里只是作为一个例子,我们仅仅实现一些简单的操作即可:
//+------------------
//
// 简单的字符串处理类
//
class String{
public:
//
// 构造函数
//
String(const char* str = "") :mData(nullptr){
int len = strlen(str) + 1;
mData = new char[len];
memset(mData, 0, len );
memcpy(mData, str, len - 1);
}
//
// 析构函数
// 该函数绝不能使用缺省的,我们必须手动释放资源
//
~String(){
if (mData){
delete[] mData;
mData = nullptr;
}
}
//
// 复制构造函数
// 该函数不能使用缺省的,我们必须手动拷贝资源
//
String(const String& str):mData(nullptr){
int len = str.size();
len += 1;
mData = new char[len];
memset(mData, 0, len);
memcpy(mData, str.mData, len - 1);
}
//
// 赋值操作符
// 该函数不能使用缺省的,我们必须手动拷贝资源
//
String& operator=(const String& str){
if (this == &str){
return *this;
}
if (mData != nullptr){
delete[] mData;
mData = nullptr;
}
int len = str.size();
len += 1;
mData = new char[len];
memset(mData, 0, len);
memcpy(mData, str.mData, len - 1);
return *this;
}
//
// 获取字符串长度
//
unsigned size() const{
if (mData == nullptr){
return 0;
}
return strlen(mData);
}
//
// 下标操作符
//
char& operator[](unsigned index){
if (mData == nullptr || index >= size()){
throw std::out_of_range("operator[](unsigned index)");
}
return mData[index];
}
const char& operator[](unsigned index) const{
return const_cast<String*>(this)->operator[](index);
}
//
// 检查字符串是否为空
//
bool empty() const{
return this->size() == 0;
}
//
// 支持流的输出
//
friend std::ostream& operator<<(std::ostream& os, const String& str){
if (str.empty()){
return os;
}
os << str.mData;
return os;
}
private:
char* mData{ nullptr };
};
//
// 测试代码
//
int main(){
String str("Hello World");
std::cout <<"str = "<< str << std::endl;
String str2 = str;
std::cout << "str2 = " << str2 << std::endl;
String str3;
std::cout << str3.empty() << std::endl;
std::cout << str2.size() << std::endl;
str3 = str2;
str3[2] = 'H';
std::cout << str3.empty() << std::endl;
std::cout << str3 << std::endl;
system("pause");
return 0;
}
//+------------------
字符串的操作属于最基本的操作,但同时也是最有讲究的操作,几乎每一个相对完善的C++类库都提供有字符串处理类,比如标准库中的string,MFC和ATL的CString,Qt的QString,CEGUI的String,DuiLib的DuiString等等,所以字符串的处理虽然是基本的操作,却也是最为重要的操作,网上流传的C++面试题中更是将字符串的实现作为一大考点,当然这不足为奇,因为要是现在一个完备的字符串类,需要考虑到方方面面的东西,后续我们会提供一个功能强大的字符串类,那么余下的就由各位去思考。