C语言结构体中只能定义变量,在C++中,结构体内不仅可以定义变量,也可以定义函数。现在以C++方式实现, 会发现struct中也可以定义函数。
struct People
{
public:
void test()
{
cout << 11 << endl;
}
int _ID = 1;
int _age = 2;
char sex[10] = "male" ;
};
在C++中引入了类这个概念,引入class作为类的关键字,它和struct类似,都可以定义结构体,函数变量等等,在C++中更习惯用class。
class classname类的名字
{
//类的成员变量,包括结构体,函数,变量
public:
void Init(int year)
{
_year = year;
}
};
//最后以分号结尾
上述代码块就是C++中定义类的基本步骤,类中的函数也可以称为成员函数。
有关于类中函数的定义,还有两种方式,一种是函数的定义和声明都放在类中,另一种就是声明放在头文件,定义放在源文件内。其中第一种定义可能会被编译器当作内联函数,大家在平常学习的时候,为了方便使用第一种方法即可,往后在工作中还是选用第二种吧。
关于成员变量的命名,有的时候成员变量和函数形参可能名字冲突,因此我们在学习C++时,对于成员变量可以添加一个前缀或者后缀加以区分,如笔者上方代码所示,C++中习惯用 '_' 表示。
访问限定符说明:
(1)public修饰的成员可以在类外直接被访问。
(2)protected和protected效果类似,在类外都不能直接被访问。
(3)访问权限作用域从该访问限定符出现的位置开始知道下一个访问限定符出现。如果后面没有访问限定符,作用域直到 '}' 结束。
(4)class的默认访问权限为private,struct为public。
C++中class和struct的区别?
在上篇博客中我们简单提及了class和struct,今天我们简单说说的区别是什么。
C++需要兼容C语言,因此struct在功能上和class一样,都可以定义类。区别是class的默认权限是private,struct的默认权限是public(要兼容C),同时在继承和模板参数列表位置,struct和class也有区别,往后学习中再向大家介绍。
C++作为面向对象的编程语言,有三大特性:封装、继承、多态。
类和对象主要体现了封装这一特性。所谓封装,就是将对象的细节隐藏起来,只留下一些接口提供给使用者用来与对象交互。通过封装的方式,将对象内部具体细节隐藏起来,更加方便管理。
类定义了一个新的作用域,类的所有成员都在类的作用域中。在类体外定义成员时,需要使用 :: 作用域操作符指明成员属于哪个类域。具体如下方代码所示:
class people
{
public:
int mul(int a, int b);
};
int people::mul(int a, int b)
{
return a * b;
}
类顾名思义,就是对类中成员进行描述,是一个模型一般的东西,规定对象的一些共性。同时类本身是没有分配实际的内存空间存储它,类更像是圈定的一个范围,一张写好的表格,而用类类型创建对象才是实际信息,被定义的对象占用实际的物理空间,存储类成员变量,这一过程也就是类的实例化。
可能有些朋友初学C++时对成员和对象有些分不清楚,这里简单说说:
类中创建的变量,结构体,函数是类的成员,而用某一类型去创建的变量,才是对象,一个在类内部,一个在类外部。
刚才我们说了被定义的对象才有占用实际的空间,那类似结构体一般,类中定义了函数,变量,我们又该怎么计算大小呢?
class A1
{
void B1();
int a = 1;
};
class A2
{
void B2();
};
class A3
{
};
由上我们可以看出,在C++中,计算类的大小只考虑类中变量的大小,而函数与结构体不考虑,如果只有函数大小当作1,空类的大小也是1,用来标识类的对象。
同时,C++类的大小也要注意内存对齐,规则与C语言结构体内存对齐规则类似:
(1)第一个成员在与结构体偏移量为0的地址处。
(2)其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。(VS编译器默认对齐数是8)
(3)类的总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍。
(4) 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整 体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
class Date
{
public:
void init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void show()
{
cout << _year << " " << _month << " " << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date x1, x2;
x1.init(2023, 2, 10);
x2.init(2023, 2, 11);
x1.show();
x2.show();
return 0;
}
我们来看看上方代码,我们定义了两个对象x1,x2,而类中init函数和show函数只有一个,我们思考一下,编译器怎么区别调用的是x1.init 还是x2.init?
这里起作用的就是我们要学习的this指针了。C++编译器给每个非静态的成员函数增加了一个隐藏的指针参数——this指针,让该指针指向当前对象(即函数运行时调用该函数的对象)。在函数体中所有成员变量的操作都是通过该指针去访问,只不过这个操作由编译器完成,对我们用户是透明的。
(1)this指针的类型:类类型*const,即this指针是只读不写的,不能改动。
(2)只能在成员函数的内部使用
(3) this指针本质上是“成员函数”的形参,当对象调用成员函数时,将对象地址作为实参传递给 this形参。所以对象中不存储this指针。
(4) this指针是“成员函数”第一个隐含的指针形参,一般情况由编译器通过寄存器自动传递(即this指针存在于寄存器上),不需要用户传递,有些编译器也会存在栈上。
(5)this指针不能为空,但是可以通过强转化置为空,不过不推荐。
(6)this指针存在于非静态的成员函数内,并不存在于对象内.
首先我们看看下面两组程序的运行结果:
// 1. 以下程序运行的结果是: 编译错误? 运行崩溃? 正常运行?
class B1
{
public:
void show()
{
cout << "show" << endl;
}
int _a = 1;
};
int main()
{
B1* x = nullptr;
x->show();
return 0;
}
// 2.以下程序运行的结果是: 编译错误? 运行崩溃? 正常运行?
class B2
{
public:
void show()
{
cout << _a << endl;
}
int _a = 1;
};
int main()
{
B2* x = nullptr;
x->show();
return 0;
}
如上方所示,第一道题运行成功,而第二道题运行崩溃,下面笔者来解释一下:
首先,两道题都不会选择编译错误,因为编译过程中主要检查的是语法错误,而上方两组代码都没有逻辑或者语法错错误,自然也就选不到编译错误。我们在”this指针的特性“中说过,调用对象会把this指针作为形参中的一个,而第一组和第二组结果的区别就在于,第一组在使用空指针时并没有调用成员变量,因此正常运行,而第二组调用了成员变量。空指针本身没有错误,错误的在于使用空指针,这就导致了第二组运行崩溃。
在计算类的大小那一节中,我们说过空类的大小是1,但是空类真的是什么都没有吗?其实空类里编译器会自动生成六个默认成员函数——即用户没有显示实现,编译器会生成的成员函数成为默认成员函数。我们先介绍第一个——构造函数。
class Date
{
public:
void init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void show()
{
cout << _year << " " << _month << " " << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date x1, x2;
x1.init(2023, 2, 10);
x2.init(2023, 2, 11);
x1.show();
x2.show();
return 0;
}
还是上方这个例子,尽管对象x1,x2可以通过init来初始化,但是如果每个类每次创建对象都要自己调用函数初始化是不是有些麻烦,有没有可能在对象创建的时候就将信息设置进去?
C++类中的默认成员函数——构造函数就可以帮助我们解决烦恼。构造函数名字与类名相同,创建类类型对象时编译器自动调用,保证每个数据都有一个合适的初始值,且在对象的整个生命周期中只调用一次。它的功能时初始化对象,而不是开辟空间创建对象,各位不要被名字迷惑了。
(1)函数名与类名相同。
(2)可以重载。
(3)对象实例化时编译器自动调用。
(4)无返回值。
(5)如果用户没有显式定义,编译器将自动生成一个无参的默认构造函数,一旦用户生成编译器将不再生成。
class Date
{
public:
//无参构造函数, 函数名和类名相同,且不返回值,故不需要标明函数返回值类型
/* Date()
{
}*/
//全参类型构造函数
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void show()
{
cout << _year << " " << _month << " " << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date x1, x2;
x1.show();
x2.show();
return 0;
}
我们分别调用自己设计的构造函数和编译器默认的构造函数看看:
我们发现编译器对于默认构造函数初始化的居然是随机值,那到底默认构造函数的作用在哪里呢?
C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据类型,如:int、char等等,自定义类型就是我们使用class、struct、union等自己定义的类型。我们再看看以下程序观察一下:
class Time
{
public:
Time(int one = 3, int two = 3, int three = 3)
{
cout << "show time" << endl;
_one = one;
_two = two;
_three = three;
}
private:
int _one;
int _two;
int _three;
};
class Date
{
public:
//无参构造函数, 函数名和类名相同,且不返回值,故不需要标明函数返回值类型
/* Date()
{
}*/
//全参类型构造函数
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void show()
{
cout << _year << " " << _month << " " << _day << endl;
}
private:
//对基本类型在声明时可以给定初始值
int _year = 2;
int _month = 2;
int _day = 2;
Time _t;
};
int main()
{
Date x1;
x1.show();
return 0;
}
我们看看对于自定义类型,编译器会自动调用自定义类型的构造函数,当我们把Time类的构造函数改成private试试看:
通过上述代码我们发现,对于自定义类型,只要我们编写了自定义类的构造函数,不管是权限如何,或者是被注释,编译器都会尝试调用。如果我们没有编写的话,编译器则会不初始化自定义变量。(在vc2022编译器上程序可以编译通过,其他编译器笔者还未尝试过)。
另外对于内置类型,在C++11里,允许对类中的内置类型在声明时给予默认值,如下图所示:
我们看到,在光标移动到191行时,即定义变变量但还未进入构造函数的时候,三个内置类型变量被初始化为2、2、2.另外,为了大家方便查看,因此没有全部截图,代码仍和上方代码一致,只是为了方便调试观看调整了一下位置,有兴趣的朋友可以自己试试看,源码如下:
class Time
{
public:
Time(int one = 3, int two = 3, int three = 3)
{
cout << "show time" << endl;
_one = one;
_two = two;
_three = three;
}
private:
int _one;
int _two;
int _three;
};
class Date
{
public:
//对基本类型在声明时可以给定初始值
int _year = 2;
int _month = 2;
int _day = 2;
Time _t;
//无参构造函数, 函数名和类名相同,且不返回值,故不需要标明函数返回值类型
/* Date()
{
}*/
//全参类型构造函数
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void show()
{
cout << _year << " " << _month << " " << _day << endl;
}
};
int main()
{
Date x1;
x1.show();
return 0;
}
至此我们可以总结一下构造函数的又一特性:
(6)对于自定义类型,只要我们编写了自定义类的构造函数,不管是权限如何,或者是被注释,编译器都会尝试调用。如果我们没有编写的话,编译器则会不初始化自定义变量。(如果对自定义变量没有自己编写构造函数,编译器可能编译不过,vs下可以编译通过)。对于内置类型,在C++11里,允许对类中的内置类型在声明时给予默认值。
上节构造函数的学习,我们在知道了类中对象怎么自动初始化的,那对象能不能自动销毁呢?
答案是有的——类中的默认成员函数析构函数就专门完成这一任务。析构函数的功能与构造函数相反,它会销毁对象调用中占用的资源,而局部对象本身的销毁会交给编译器完成。
(1)析构函数名是在类名前加上字符~。
(2)无参数类型无返回值类型
(3)一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。(注意:析构 函数不能重载)
(4)对象生命周期结束时,编译系统自动调用析构函数
同样的,类比构造函数,我们看看析构函数对于内置类型和自定义类型如何处理。请看下方代码:
class Time
{
public:
Time(int one = 3, int two = 3, int three = 3)
{
cout << "show time" << endl;
_one = one;
_two = two;
_three = three;
}
~Time()
{
cout << "Time" << endl;
}
private:
int _one;
int _two;
int _three;
};
class Date
{
public:
//对基本类型在声明时可以给定初始值
int _year = 2;
int _month = 2;
int _day = 2;
Time _t;
//无参构造函数, 函数名和类名相同,且不返回值,故不需要标明函数返回值类型
/* Date()
{
}*/
//全参类型构造函数
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void show()
{
cout << _year << " " << _month << " " << _day << endl;
}
};
int main()
{
Date x1;
x1.show();
return 0;
}
我们先看看在不同类中设置自定义类型。由下图我们可以发现Date类在最后销毁时调用了~Time,而且它并不是直接调用,转到反汇编我们发现Date类自动生成了~Date,调用的是~Date,但没有调用~Time,由此我们推断:对于自定义数据类型,如果在不同类,编译器会在本类生成一个析构函数,在内部调用自定义数据类型的析构函数。
以上是我们编写了自定义数据类型析构函数的情况,那如果我们注释或者不编写呢?
为了方面调试,我们自定义一个Time*的指针用来存储一个全局变量的地址,观察不编写自定义数据类型的析构函数时会有什么情况:
class Time
{
public:
Time(int one = 3, int two = 3, int three = 3)
{
cout << "show time" << endl;
_one = one;
_two = two;
_three = three;
}
/*~Time()
{
cout << "Time" << endl;
}*/
private:
int _one;
int _two;
int _three;
};
int x = 1;
class Date
{
public:
//对基本类型在声明时可以给定初始值
int _year = 2;
int _month = 2;
int _day = 2;
Time* _t = (Time*) & x;
//无参构造函数, 函数名和类名相同,且不返回值,故不需要标明函数返回值类型
/* Date()
{
}*/
//全参类型构造函数
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void show()
{
cout << _year << " " << _month << " " << _day << endl;
}
};
int main()
{
Date x1;
printf("%p\n", x1._t);
x1.show();
return 0;
}
由上图我们发现,在光标移动到210行与214行时,_t都没有被清理,也就说明对于自定义数据类型,如果我们不编写析构函数,编译器也不会清理(注释掉析构函数也是一样的结果,就不重复演示了)。
那如果只有内置类型呢,我们再观察一下。
由上方运行结果我们看到,当程序走到末尾时,_year的值还存在,而编译器也没有调用析构函数,这是怎么回事呢?
大家回想一下以往我们学习,也常常使用内置类型,为什么不需要在程序末尾清理呢?实际上,对于内置类型,不需要调用析构函数,系统会自动在程序结束时收取其内存空间。
由上我们可以总结出析构函数的又一特性:
(5)对于内置类型,销毁时不需要调用析构函数,系统在最后将其内存空间直接收回。对于自定义类型,和上节构造函数类似,编译器会调用自定义类型的析构函数。如果自定义类型数据所在类其本身类类型不同,则会在本类中生成一个默认析构函数,由它在内部调用自定义类型数据对应的析构函数,而不是直接调用。如果没有编写对应的析构函数,编译器也就不做处理。
有细心的朋友可以发现,在观察编译器对不同类型数据调用析构函数的情况时,用了两种不同的例子——无资源申请的和有资源申请的。结合开头对析构函数的介绍,我们大致可以了解析构函数的一些作用:
对于类中无资源申请的情况,我们可以不写析构函数。对于类中有资源申请的情况,则一定要写,可以减少内存泄漏,资源浪费的情况。
上面我们介绍构造函数和析构函数,那针对我们创建的对象,编译器有没有帮我们复制对象的函数呢?
答案是有的——即拷贝构造函数。拷贝构造函数只有单个形参,此形参是对本类类型对象的引用,常用const修饰,在用已存在的类类型对象创建新对象时由编译器自动调用。
(1)拷贝构造函数时构造函数的一个重载形式。
(2)拷贝构造函数的参数有且只有一个,并且必须时类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用。
(3)如果没有显示定义,编译器会生成默认的拷贝构造函数。默认的拷贝构造函数对象按内存存储字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。
对于内置类型和自定义类型,编译器会采取什么措施呢?如果有朋友看了上面两节关于构造函数和析构函数的解释,可能心里会有一些概念,这里我们再演示一下:
class Time
{
private:
int _one;
int _two;
int _three;
};
class Date
{
public:
//对基本类型在声明时可以给定初始值
int _year = 2;
int _month = 2;
int _day = 2;
//构造函数
Date() {}
//拷贝构造函数
/*Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}*/
void show()
{
cout << _year << " " << _month << " " << _day << endl;
}
};
int main()
{
Date x1;
x1.show();
Date x2(x1);
return 0;
}
我们知道如果显式定义了拷贝构造函数,编译器会自动调用,上方代码中笔者注释掉拷贝构造函数,发现对于内置类型,尽管在反汇编中没有类似call Date(const Date& d)的操作,但是我们可以看到当代码运行到Date x2(x1), x2和x1分别被放在了寄存器中,最后一行byte ptr,如果有朋友自己实现过memcpy等函数就会发现,这是字节拷贝常用的手段,结合前面的movs,大致可以推断出这里编译器将x1拷贝给x2了(为了简单说说,这里就尽量不涉及汇编的知识)。
尽管编译器没有明显地调用默认的拷贝构造函数,但是对于内置类型,还是自动按字节拷贝了。
那对于自定义类型呢?我们再往下看看:
为了方便查看,我们在Date类中加入自定义类型,这个自定义类型和Date并不是同一类,并且编写对应的拷贝构造函数。
class Time
{
private:
int _one;
int _two;
int _three;
public:
Time()
{
_one = 1;
_two = 1;
_three = 1;
}
Time(const Time& t)
{
_one = t._one;
_two = t._two;
_three = t._three;
cout << "Time" << endl;
}
};
class Date
{
public:
//对基本类型在声明时可以给定初始值
int _year = 2;
int _month = 2;
int _day = 2;
Time _t;
//构造函数
//Date() {}
//拷贝构造函数
/*Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}*/
void show()
{
cout << _year << " " << _month << " " << _day << endl;
}
};
int main()
{
Date x1;
x1.show();
Date x3;
Date x2(x1);
return 0;
}
由上图我们可以看到,x1、x2、x3分别调用了Date函数,但由后面的数字我们可以发现x2调用的是拷贝构造函数,同时运行结果也能反映这一点,结合编译器对内置类型的处理,我们可以推断出如果有自定义类型,编译器会调用自定义类型的拷贝构造函数,而如果自定义类型本类和所处类不同,则会生成默认的拷贝构造函数在内部调用。由此我们可以总结出拷贝构造的又一特性:
(4)对于内置类型,编译器会调用默认的拷贝构造函数,如果用户显式编写了,就会调用用户编写的。对于自定义类型,编译器会调用其对应的拷贝构造函数,如果不在同一类型,则会生成默认的拷贝构造函数再去调用。
接下来我们思考一个问题?编译器会自动生成拷贝构造函数,那还需要我们自己编写吗?
参考析构函数,我们可以大致推断出,对于类中不申请资源占用,则可以不写,但是对于涉及资源申请的情况,则需要写。
我们在上文说过,默认的拷贝构造函数是浅拷贝(值拷贝),顾名思义,只拷贝值,按照字节序拷贝,一旦涉及到资源申请,那么拷贝后的成员会和原来成员指向同一空间,这就不是我们想要的结果了。而涉及到资源申请,就需要深拷贝解决了,这就留到后面再说了。
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似,同时也是类的默认成员函数之一。对于刚初步了解的运算符重载,有一些基础特点要注意:
(1)运算符重载的函数名字为:关键字operator后接需要重载的运算符符号。
(2)不能通过连接其他符号来创建新的操作符:比如operator@。
(3)重载操作符必须有一个类类型参数,且重载操作符的参数列表应该由其本身的含义决定,即操作符本身有几个操作对象,就应该有几个参数,如==,==是二目操作符,因此对其重载时应该有两个参数。另外,对于三目操作符没有重载。
(4)用于内置类型的运算符,不能改变符号本身的含义,例如内置的整型+,我们可以用它来实现日期加,不能实现日期拷贝,因为不能改变这个符号的含义。
(5)作为类成员函数重载时,其形参看起来比操作数目少1,因为成员函数的第一个参数为隐藏的this。
(6).* 、 :: 、sizeof、 ?:、 . 注意这五个运算符不能重载。其中.*基本不用,知道就行,在MATLB中可能更常见。::是作用域操作符,?:是三目操作符,在学习C语言时大家应该了解过。.是结构体或者类访问成员对象时用到的,可以理解为成员选择操作符。
接下来,我们尝试写一个operator==:
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void show()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
//operator==
bool operator==(const Date& d)
{
return _year = d._year && _month == d._month && _day == d._day;
}
int _year;
int _month;
int _day;
};
//bool operator==(const Date& d1, const Date& d2)
//{
// return d1._year == d2._year && d1._month == d2._month && d1._day == d2._day;
//}
void Test()
{
Date d1(2023, 2, 16);
Date d2(2023, 2, 16);
cout << (d1 == d2) << endl;
}
int main()
{
Test();
return 0;
}
对于上述代码,有几点想说的:
第一点,我们知道调用类中不同成员的权限是不同的,而在使用重载后的操作符,怎么保证可以顺利调用呢?
就我们现在所学的来说,可以把类中的权限全都置为pubilic,这样就方便调用,另一种方法就是把运算符重载也写进类里。针对这点,写出了两种实现方法,大家可以参考一下。当然,以上两种方法都不算太妥当,后续学习友元函数时会帮助我们解决这个问题。
第二点,对于在类中实现的方法,有些朋友可能会疑惑?==在实现操作符重载时,应该是两个参数,为什么只写了一个。
上方提到过,类中函数有第一个参数是隐藏的this指针,而这个隐藏的this指针就是我们的第一个形参。
//所以在类中的operator==也可以写成以下形式:
//operator==
bool operator==(const Date& this,const Date& d)
{
return this._year = d._year && this._month == d._month && this._day == d._day;
}
//当然,C++支持运算符重载本身就是为了支持代码的可读性,我们也就没必要写的这么明白了。
(1)参数类型:const T&,尽量使用传引用,减少拷贝提高传参效率
(2)返回值类型:T&,返回使用引用可以提高效率,而返回值也可以支持连续赋值,更加便利。
(3)返回*this,要复合连续赋值的含义。拿operator=举例子:
//operator=
Date& operator=(const Date& d)
{
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
void Test()
{
Date d1(2023, 2, 16);
Date d2(2023, 2, 16);
Date d3(1,1,1);
d3 = d2 = d1;
cout << (d1 == d2) << endl;
}
//在d3 = d2 = d1中,d2 = d1的结果作为返回值传递给d3,右值不能为void,因此this指针就派上了用场,*this把结果传递给d3,依次来实现连续的赋值,且this指针指向的对象在对象函数结束后不会销毁,故*this方式返回效率更高
(4)检测是否需要自己给自己赋值
(1)赋值运算符只能重载成类的成员函数不能重载成全局函数
如果我们在类外重载赋值运算符,会发现编译器报错,且无法编译通过。这是因为赋值运算符如果不显式实现,编译器就会自动生成一个默认的,此时用户在类外自己实现一个全局的赋值重载运算符,就和默认的冲突了,故赋值运算符重载,只能在类内实现。
(2)用户没有显式实现时,编译器会生成一个默认的赋值运算符重载,以值的形式拷贝,对于内置类型成员变量编译器直接赋值,而对于自定义类型的成员变量则需要调用对应类的赋值运算符重载完成赋值。
其实说到这,大家可能发现拷贝构造函数和赋值运算符重载机制很类似,而构造函数和析构函数也和类似。参考拷贝构造函数,对于自定义类型,如果不涉及资源申请,那是否实现赋值运算符重载都行,但是对于类中有资源申请的情况,则一定要实现对于的赋值运算符重载。
在单目操作符里,++比较特别,因为它有两种使用方式——前置++和后置++,由此在重载时也显得不太一样,下面我们仔细说说:
(1)前置++
//前置++,单目操作符
Date& operator++()
{
_day += 1;
return *this;
}
如上方代码所示,前置++在过程上会简单点,先++再传值,故可以像之前实现operator=一样传引用返回,另外由于我们实现的是日期类,只需要实现day的加法就行。后续的进位这里暂时不考虑了。
(2)后置++
//后置++
Date operator++(int)
{
Date tmp(*this);
*this += 1;
return tmp;
}
后置++的第一个问题就是参数问题,++作为单目运算操作符,按道理只有一个参数this,而由其特殊性质,故C++引入了一个参数int用于区分前置++和后置++,以便于二者可以形成正确重载。而这个int参数,在调用时不参与传递,编译器会自动传递。
关于后置++的实现:
由于后置++是先传递值再++,这就要利用临时变量来传递,因此不能使用传引用。(在函数调用结束后,临时变量会销毁)在临时变量的保存上,借用隐藏的this指针。在调用构造函数时,一个个传递成员对象可以,直接传递==*this==指针也行。不过既然设计之初就是为了方便阅读,笔者觉得传==*this==更好一些。
讲到这里,我们对了四大默认成员函数已经有了一定的了解,为了更好掌握它们,不妨写一个日期类试试。各位有兴趣的看看这篇博客:
C++——万字博客详解类和对象
将const修饰的”成员函数“称之为const成员函数,const修饰类成员函数,实际修饰该成员函数的this指针,表明在该成员函数中不能对类的任何成员进行修改。
我们知道在类中的函数,第一个参数默认为this指针,当我们传参进去时,如果涉及到const修饰的常量就麻烦了,因为*this指针并没有被const修饰,不具有不被修改的属性,这样的传参就使得权限放大,权限可以缩小,但是不可以扩大。同时我们在类外传参的时候又不好修饰const,由此c++支持const修饰成员函数,用来修饰this指针。
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void printf()
{
printf1();
cout << _year << "-" << _month << "-" << _day << endl;
}
void printf1() const
{
printf();
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2022,2,2);
const Date d2(2022,2,3);
d1.printf1();
}
根据以上代码,以及我们对const成员函数的介绍,我希望大家想想以下几个问题(很简单的,不明白的朋友自己应代码试一试):
(1)const对象可以调用非const成员函数吗?
(2)非const对象可以调用const成员函数吗?
(3)const成员函数内可以调用其他的非const函数吗?
(4)非const成员函数内可以调用其他const成员函数吗?
别的操作符可以重载,同样取地址及const取地址操作符也可以重载,这两个默认成员函数基本不用重新定义,编译器会默认自动生成,因此仅仅只是简单介绍一下:
class Date
{
public:
//取地址重载思路:参数传递由this指针接收,就传递this指针回去就好了
Date* operator&()
{
return this;
}
//const取地址操作符重载思路:和取地址重载一样,只是加了const
const Date* operator&() const
{
return this;
}
};
int main()
{
Date d1;
cout << &d1 << endl;
return 0;
}
我们创建对象时,可以编写构造函数进行初始化,然而并不能算初始化,只能说给成员变量一个初始值,因为初始化只能初始化一次,而我们可以反复调用构造函数,准确来说,构造函数是给成员变量赋值而不是初始化。因此,我们初始化我们的成员变量,就引入了初始化列表。
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
(1)初始化列表格式:以一个冒号开始,接着是逗号分隔的数据成员列表,每个成员变量后面跟一个括号,括号内放置变量的初始值或者表达式。且每个成员变量只能在初始化列表中出现一次(初始化只能初始化一次)。
class Date
{
public:
Date(int year, int month, int day)
: _year(year)
, _month(month)
, _day(day)
{}
void show()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2002,1,1);
d1.show();
return 0;
}
(2)对于类中的这类成员变量,必须在初始化列表位置进行初始化:引用成员变量、const成员变量、自定义类型成员变量(且该类没有默认构造函数,同时要注意只要是静态成员就必须在类外初始化,不论是static,还是static const)
class A
{
public:
A(int a)
{
_a = 1;
}
private:
int _a;
};
class B
{
public:
B(A B1, int& B2, const int B3)
:_B1(B1)
,_B2(B2)
,_B3(B3)
{}
private:
A _B1;//无默认构造函数的自定义类型
int& _B2;//引用类型
const int _B3;//cosnt修饰
};
(3)能使用初始化列表初始化就使用初始化列表初始化,因为不管是否使用初始化列表,对于自定义类型,一定会优先使用初始化列表初始化。
class A
{
public:
A(int a = 1)
:_a(a)
{
cout << "没事就用初始化列表" << endl;
}
private:
int _a;
};
class B
{
public:
B(int b1)
{}
private:
int _b1;
A _a;
};
int main()
{
B B1(1);
return 0;
}
(4)成员变量在类中的声明顺序就是在初始化列表中的初始化顺序,与其在初始化列表中的先后顺序无关。
class A
{
public:
A(int a)
:_a1(a)
, _a2(_a1)
{}
void print()
{
cout << _a1 << "-" << _a2 << endl;
}
private:
int _a2;
int _a1;
};
int main()
{
A B1(1);
B1.print();
return 0;
}
我们看看以上代码的结果是什么:
由于先声明d2,再声明d1,因此进入初始化列表后,d2无法初始化,故为随机值,d1为1.
概念:声明为static的类成员称之为类的静态成员,用static修饰的成员变量,称之为静态成员变量,用static修饰的成员函数,称之为静态成员函数。静态成员变量一定要在类外进行初始化。
特性:
(1)静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区
(2)静态成员变量必须在类外全局处定义,定义时不添加static关键字,类中只是声明。
(3)类静态成员即可用类名::静态成员或者对象.静态成员来访问,静态成员不属于那个对象,只是可以通过这种方式访问静态成员。
(4)静态成员函数没有隐藏的this指针,不能访问任何非静态成员。但非静态成员函数可以访问静态成员函数。(有点像const修饰的成员函数)
(5)静态成员也是类的成员,受public、protected、private访问限定符的限制。
class A
{
public:
A(int a1, int a2)
:_a1(a1)
, _a2(a2)
{}
void print()
{
cout <<1111111 << endl;
print1();
}
static void print1();
private:
int _a2;
int _a1;
static int x1;
};
int A::x1 = 1;
void A::print1()
{
cout << 22222 << endl;
}
int main()
{
A B1(1,1);
B1.print1();
return 0;
}
友元提供了一种突破封装的方式,有的时候提供了遍历,但是友元会破坏稳定性,破坏了封装,所有不宜过多使用友元。
友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加friend关键字。在我们之前实现过的日期类中,对于operator<<和operator>>就使用友元。
友元函数的一些说明:
(1)友元函数可以访问类的私有成员和保护成员,但不是类的成员函数。同时友元函数可以在类定义的任何地方声明,不受访问限定符的限制。
(2)友元函数不能用const修饰。
(3)一个函数可以是多个类的友元函数。
(4)友元函数的调用和普通函数的调用原理相同。
(1)友元类中所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。
(2)友元关系是单向的,不具有交换性。比如A类是B类的友元类,则A类可以访问B类,但是B类不能访问A的成员函数。
(3)友元关系不能传递。比如A是B的友元类,B是C的友元类,但是不能说明A是C的友元类。
(4)友元关系不能继承。(后续介绍继承时和大家说明)
class A
{
public:
friend void print();//友元函数
A() {}
private:
friend class B;//友元类,不受限定符影响
};
class B
{
};
int main()
{
return 0;
}
如果一个类定义在另一个类的内部,则这个类就叫做内部类。
(1)内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何特别的访问权限。
(2)内部类就是外部类的友元类,内部类可以通过外部类的对象参数来访问外部类中的所有成员,但是外部类不是内部类的友元类。
(3)内部类可以定义在外部类的任意限定区域,没有限制。
(4)内部类还可以直接访问外部类中的static成员,不需要外部类的对象或者类名。
(5)sizoef(外部类)== 外部类,和内部类无关。
class A
{
private:
int _a1 = 1 ;
static int _a2;
public:
class B
{
public:
void look(const A& a)
{
cout << a._a1 << endl;
cout << _a2 << endl;
}
};
};
int A::_a2 = 2;
int main()
{
A::B b1;
b1.look(A());//?
return 0;
}
细心的朋友们可能发现了,我们在调用look时,没有传递具体的A对象,这又是为什么呢?
这就涉及到接下来我们要讲的匿名对象。
匿名对象,即不需要取名字的对象,它的声明周期只有一行,在下一行编译器就会自动调用析构函数。进行销毁。在某些场景下匿名对象比较方面,比如我们要调用类中的某个函数,或者如上方内部类的例子一样,将某个类作为参数,或者初始值调用,这个时候用匿名对象就比较方便。
class A
{
public:
A(int a)
:_a(a)
{}
~A() {}
private:
int _a;
};
int main()
{
A(1);
return 0;
}
进入反汇编,我们可以看到A(1)调用了构造函数后紧接着就调用了析构函数。