概念概述
- 默认成员函数就是用户没有显式实现,编译器会自动生成的成员函数称为默认成员函数。一个类,我们不写的情况下编译器会默认生成以下 6 个默认成员函数,需要注意的是这 6 个中最重要的是前 4 个,最后两个取地址重载不重要,我们稍微了解一下即可。其次就是 C++11 以后还会增加两个默认成员函数,移动构造和移动赋值,这个我们后面再讲解。默认成员函数很重要,也比较复杂,我们要从两个方面去学习。
包括
构造函数
概念概述
构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象 (我们常使用的局部对象是栈帧创建时,空间就开好了),而是对象实例化时初始化对象。构造函数的本质是要替代我们以前 Stack 和 Date 类中写的 Init 函数的功能,构造函数自动调用的特点就完美的替代的了 Init。
构造函数的特点:
- 函数名与类名相同。
- 无返回值。(返回值啥都不需要给,也不需要写 void,不要纠结,C++ 规定如此)
- 对象实例化时系统会自动调用对应的构造函数。
- 构造函数可以重载(因为需要不同的数据,不同的初始化,会使开发更加简单)。
- 如果类中没有显式定义构造函数,则 C++ 编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。
- 无参构造函数、全缺省构造函数、我们不写构造时编译器默认生成的构造函数,都叫做默认构造函数。但是这三个函数有且只有一个存在,不能同时存在。无参构造函数和全缺省构造函数虽然构成函数重载,但是调用时会存在歧义。要注意很多同学会认为默认构造函数是编译器默认生成那个叫默认构造,实际上无参构造函数、全缺省构造函数也是默认构造,总结一下就是不传实参就可以调用的构造就叫默认构造。
- 我们不写,编译器默认生成的构造,对内置类型成员变量的初始化没有要求,也就是说是是否初始化是不确定的,看编译器。对于自定义类型成员变量,要求调用这个成员变量的默认构造函数初始化。如果这个成员变量,没有默认构造函数,那么就会报错,我们要初始化这个成员变量,需要用初始化列表才能解决,初始化列表,我们下个章节再细细讲解。
- 简单的说构造函数就是用来初始化的函数,而且默认构造函数会自动调用
注意:
C++ 把类型分成内置类型 (基本类型) 和自定义类型。内置类型就是语言提供的原生数据类型,如:int/char/double/ 指针等,自定义类型就是我们使用 class/struct 等关键字自己定义的类型。
构造函数的实现
不带参数构造函数
//.h ///默认构造函数的使用 class MyClass { public: //默认构造函数 MyClass()//类名相同,没有参数,叫做无参数构造函数 { _year = 1999; _month = 12; _day = 1; cout << "默认构造函数的自动调用:" ; cout << _year << "/" << _month << "/" << _day << endl; } private: int _year; int _month; int _day; }; //.cpp int main() { MyClass d1; return 0; }
此时我们发现这里在初始化的时候自动调用无参数构造函数(也就是默认构造函数),当然这里初始化成功
带参数构造函数
//.h class MyClass1 { public: MyClass1(int year, int month, int day) { _year = year; _month = month; _day = day; cout << "带参数构造函数的调用:"; cout << _year << "/" << _month << "/" << _day << endl << endl; } private: int _year; int _month; int _day; }; //.cpp int main() { //不带参数构造函数 MyClass d1; //带参数构造函数 MyClass1 d2(2024, 9, 14);//带参构造函数这里是需要传递一个参数 return 0; }
半缺省构造函数
//.h //半缺省构造函数(从右往左进行赋值) class MyClass2 { public: MyClass2(int year, int month, int day = 28) { _year = year; _month = month; _day = day; cout << "半缺省构造函数(从右往左进行赋值):"; cout << _year << "/" << _month << "/" << _day << endl << endl; } private://私有变量,只能类里面的函数进行访问 int _year; int _month; int _day; }; //.cpp int main() { //不带参数构造函数 MyClass d1; //带参数构造函数 MyClass1 d2(2024, 9, 14); //半缺省构造函数(从右往左进行赋值),赋值的时候,从左往右哦,就是为了和构造函数的缺省参数进行分开 MyClass2 d3(2000, 2); return 0; }
半缺省构造参数(从右往左进行赋值),赋值的时候,从左往右哦,就是为了和构造函数的缺省参数进行分开
全缺省构造函数
//全缺省构造函数(从右往左进行赋值) class MyClass2 { public: MyClass2(int year, int month, int day = 28) { _year = year; _month = month; _day = day; cout << "半缺省构造参数(从右往左进行赋值):"; cout << _year << "/" << _month << "/" << _day << endl << endl; } private://私有变量,只能类里面的函数进行访问 int _year; int _month; int _day; }; int main() { //不带参数构造函数 MyClass d1; //带参数构造函数 MyClass1 d2(2024, 9, 14); //半缺省构造函数(从右往左进行赋值) MyClass2 d3(2000, 2); //全缺省构造函数 MyClass3 d4; return 0; }
默认构造函数
无参构造函数、全缺省构造函数、我们不写构造时编译器默认生成的构造函数,都叫做默认构造函数。但是这三个函数有且只有一个存在,不能同时存在。无参构造函数和全缺省构造函数虽然构成函数重载,但是调用时会存在歧义。要注意很多同学会认为默认构造函数是编译器默认生成那个叫默认构造,实际上无参构造函数、全缺省构造函数也是默认构造,总结一下就是不传实参就可以调用的构造就叫默认构造。
总结:
简单的说就是,在创建对象初始化的时候,会自动调用的函数,带参数构造函数不是默认构造函数,是需要调用的时候传参的。默认构造函数在调用创建对象的时候是不需要传参的。上面我们的代码很清晰了。
构造函数注意事项
对于自定义类型来说,构造函数往往是需要自己实现的。因为自定义类型的初始化可能较为复杂,需要根据特定的逻辑来进行成员变量的初始化等操作。例如,一个包含多个成员变量且有特定初始化需求的自定义类,通常需要定义构造函数来确保对象在创建时被正确初始化。
对于内置类型来说是不需要的。内置类型如 int、char、double 等,在创建对象时如果不进行显式初始化,它们会被自动赋予默认值(对于数值类型通常为 0,对于指针类型通常为 nullptr 等)。当然,如果需要特定的初始值,也可以在定义变量时进行显式初始化,但一般情况下不需要专门为内置类型定义构造函数。
再探构造函数
概念概述
- 之前我们实现构造函数时,初始化成员变量主要使用函数体内赋值,构造函数初始化还有一种方式,就是初始化列表,初始化列表的使用方式是以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个 “成员变量” 后面跟一个放在括号中的初始值或表达式。
- 每个成员变量在初始化列表中只能出现一次,语法理解上初始化列表可以认为是每个成员变量定义初始化的地方。
- 引用成员变量,const 成员变量,没有默认构造的类类型变量,必须放在初始化列表位置进行初始化,否则会编译报错。
- C++11 支持在成员变量声明的位置给缺省值,这个缺省值主要是给没有显示在初始化列表初始化的成员使用的。
- 尽量使用初始化列表初始化,因为那些你不在初始化列表初始化的成员也会走初始化列表,如果这个成员在声明位置给了缺省值,初始化列表会用这个缺省值初始化。如果你没有给缺省值,对于没有显示在初始化列表初始化的内置类型成员是否初始化取决于编译器,C++ 并没有规定。对于没有显示在初始化列表初始化的自定义类型成员会调用这个成员类型的默认构造函数,如果没有默认构造会编译错误。
- 初始化列表中按照成员变量在类中声明顺序进行初始化,跟成员在初始化列表出现的的先后顺序无关。建议声明顺序和初始化列表顺序保持一致。
- 初始化列表总结:无论是否显式写初始化列表,每个构造函数都有初始化列表;无论是否在初始化列表显示初始化,每个成员变量都要走初始化列表初始化。
成员变量初始化列表逻辑
再探构造函数存在的意义
//比如我们写一个栈,这里就不需要写构造函数,因为编译器默认生成队列的构造函数,调用了stack的默认构造,完成了两个成员函数的初始化
//但是如果栈的默认构造是没有的情况下,此时怎么处理?所以就需要延伸到再探构造函数
初始化列表的格式
之前我们进行初始化是函数体的初始化,现在称之为初始化列表的初始化
初始化列表的使用方式是以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个 “成员变量” 后面跟一个放在括号中的初始值或表达式。
使用的格式记住就可以
哪些成员必须采用初始化列表(没有默认构造函数的,需要使用初始化列表)
//构造函数 Date(int year = 1901, int month = 1, int day = 1); ////构造函数 //Date::Date(int year, int month, int day) //{ // _year = year; // _month = month; // _day = day; //} //构造函数,初始化列表的实现 Date::Date(int year, int month, int day) :_year(111) , _month(111) , _day(111) {} int main() { // 明确传递参数,使用带参数的构造函数和初始化列表进行初始化 Date d1(2024, 9, 27); // 不传递参数,使用默认参数值和初始化列表进行初始化 Date d2; return 0; } //不是单纯地调用默认构造函数,而是根据是否传递参数来决定使用带参数的构造函数(带默认参数),并且始终会使用初始化列表进行初始化。 //如果传递参数调用构造函数,如Date d1(2024, 9, 27);,会优先使用初始化列表,按照给定的参数值初始化成员变量_year、_month和_day。 //如果不传递参数,如Date d2;,则会使用构造函数中的缺省参数值,并同样通过初始化列表来初始化成员变量。
哪些成员必须采用初始化列表(const修饰的成员变成必须使用初始化列表)
一,const 成员函数的特性
const 成员函数承诺不修改对象的数据成员,即它保证了在函数执行期间,对象的状态不会被改变。这意味着 const 成员函数应该只能读取对象的状态,而不能进行任何可能改变对象状态的操作。
二,函数体初始化与初始化列表的区别
- 函数体初始化:在函数体内部进行成员变量的初始化是通过赋值操作来完成的。这意味着先调用默认构造函数(如果有)创建成员变量,然后再对其进行赋值。这种方式可能会导致对象被临时创建和赋值两次,增加了不必要的开销,并且在一些情况下可能违反 const 成员函数的不修改对象状态的承诺。
- 初始化列表:初始化列表在对象创建时直接初始化成员变量,避免了先调用默认构造函数再赋值的过程。对于 const 成员变量和引用成员变量,必须在初始化列表中进行初始化,因为它们不能在创建后被重新赋值。对于普通成员变量,使用初始化列表也可以提高效率,并且更符合 C++ 的初始化语义。
代码实现:这里主要看成员变量_y
#include<iostream> using namespace std; class Date { public: Date(int& z); Date(int x, int& z); Date(int x, int y, int& z); void _print() { cout << _x << "/" << _y << "/" << _z << endl << endl; } private: int _x; const int _y; int& _z; }; //再探构造函数1 Date::Date(int& z) :_x(111) , _y(111) , _z(z) {} //再探构造函数2 Date::Date(int x, int& z) :_x(222) , _y(222) , _z(z) {} //再探构造函数3 Date::Date(int x, int y, int& z) : _y(333) ,_z(z) { _x = x; } int main() { //再探构造函数1 int i1 = 0; Date d1(i1); d1._print(); cout << i1 << endl << endl; //再探构造函数2 int i2 = 0; Date d2(1, i1); d2._print(); cout << i2 << endl << endl; //再探构造函数3 int i3= 0; Date d3(1, 1, i3); d3._print(); cout << i3 << endl; return 0; }
哪些成员必须采用初始化列表(引用成员变量的初始化)
一、引用的特性
引用在 C++ 中必须在定义时被初始化,并且一旦初始化后就不能再绑定到其他对象。引用本质上是一个对象的别名,它总是指向一个特定的对象,不能被重新赋值为指向另一个不同的对象。
二、构造函数的执行过程
- 当一个对象被创建时,构造函数首先执行初始化列表来初始化成员变量。如果不在初始化列表中初始化引用成员变量,那么在构造函数体中就没有机会再对其进行初始化了,因为此时引用必须已经被绑定到一个对象。
- 如果试图在构造函数体中对引用成员变量进行赋值,这会被编译器视为重新绑定引用,而这是不允许的,会导致编译错误。
三、左值和右值的概念
- 左值(lvalue):代表一个有明确内存地址、可以取地址且有可持久性的值。通常可以出现在赋值语句的左侧,例如变量名、解引用的指针等。例如,
int a = 10;
中的a
就是一个左值,因为它有明确的内存地址,可以通过&a
取地址,并且在程序的生命周期内持续存在。引用成员变量的初始化就是不修改的左值。- 右值(rvalue):通常是临时的值,没有明确的内存地址可获取,或者是即将被销毁的值。例如,字面常量、临时对象、函数返回的临时值等。例如,
int b = 15 + 20;
这里15 + 20
的结果就是一个右值,它是一个临时值,没有独立的内存地址可获取,在表达式结束后可能就会被销毁。
代码实现
#include<iostream> using namespace std; class Date { public: Date(int& z); Date(int x, int& z); Date(int x, int y, int& z); void _print() { cout << _x << "/" << _y << "/" << _z << endl << endl; } private: int _x; const int _y; int& _z; }; //再探构造函数1 Date::Date(int& z) :_x(111) , _y(111) , _z(z) {} //再探构造函数2 Date::Date(int x, int& z) :_x(222) , _y(222) , _z(z) {} //再探构造函数3 Date::Date(int x, int y, int& z) : _y(333) ,_z(z) { _x = x; } int main() { //再探构造函数1 int i1 = 0; Date d1(i1); d1._print(); cout << i1 << endl << endl; //再探构造函数2 int i2 = 0; Date d2(1, i1); d2._print(); cout << i2 << endl << endl; //再探构造函数3 int i3= 0; Date d3(1, 1, i3); d3._print(); cout << i3 << endl; return 0; }
初始化列表的注意事项(初始化的顺序)
初始化列表的注意事项(尽量都使用初始化列表进行初始化)
这里建议尽量使用初始化列表进行初始化,因为在C++里面,他是有一套构造流程的,而且对于C++来讲,为什么比其他语言快,不仅仅的这个语言本身的特点,还是在书写的时候,我们会更加注意程序的设计
初始化列表的注意事项(建议声明和顺序保持一致)
这里也是一个需要注意的点
因为编译器进行初始化的时候,是按照头文件进行初始化的,不是你实现的顺序来进行初始化的