C++学习——类的六个默认成员函数

mac2025-08-08  13

类的6个成员成员函数

如果我们定义一个空类,编译器将会自动生成六个默认成员函数,他们分别是构造函数、析构函数、拷贝构造函数、赋值运算符重载函数、取地址操作符重载函数以及const取地址操作符重载函数。

构造函数

构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,其功能是初始化对象的数据成员,并且在对象的生命周期内只调用一次。

构造函数分为默认构造函数和用户自定义的构造函数。当用户没有显示的定义构造函数时,编译器会在对象实例化时自动生成一个空的默认构造函数,并且调用该默认构造函数为对象的数据成员初始化,当用户显示定义构造函数时,编译器则不会再次生成构造函数。 默认构造函数:

无参构造编译器默认生成的全缺省的构造函数

默认构造函数只能有一个,一般定义为全缺省构造函数,而一个类中可以有多个构造函数,在进行对象的实例化时编译器会根据参数的不同选择匹配的构造函数。

语法:

class Date { public: // 1.无参构造函数 Date () {} // 2.带参构造函数 Date (int year, int month , int day ) { _year = year ; _month = month ; _day = day ; } private: int _year; int _month; int _day; }; void TestDate() { Date d1; // 调用无参构造函数 Date d2 (2015, 1, 1); // 调用带参的构造函数 // 注意:如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明 // 以下代码是对函数的声明,不是调用构造函数:声明了d3函数,该函数无参,返回一个日期类型的对象 Date d3(); }

构造函数的特征:

函数名与类名相同。无返回值。对象实例化时编译器自动调用对应的构造函数。构造函数可以重载。
注意:

编译器默认生成的构造函数对对象的数据成员的初始化:

内置类型:无操作自定义类型:会调用自定义类型的构造函数,来对自定义类型成员初始化。 class Time { public: Time() { cout << "Time()" << endl; hour = 0; _minute = 0; _second = 0; } private: int _hour; int _minute; int _second; }; class Date { private: // 基本类型(内置类型) int _year; int _month; int _day; // 自定义类型 Time _t; //在对象实例化时,会调用Time的构造函数初始化。 };

拷贝构造函数

只有单个形参,该形参时对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。 拷贝构造函数的特征:

拷贝构造函数也是构造函数,是构造函数的一个重载形式。拷贝构造函数的参数只有一个且必须使用引用传参,使用传值方式则会引起无穷递归。如果未显示定义,编译器会生成默认的拷贝构造函数。默认的拷贝构造函数对象按内存存储字节序完成拷贝,或者值拷贝。当对象占有外部资源时,此时一定要显示定义拷贝构造函数,否则会造成内存二次释放的问题,导致程序直接崩溃。 如下面的例子: class SeqStack { public: //拷贝构造函数 /* 浅拷贝:即用户未显式定义时,编译器自动生成的拷贝构造函数 当释放空间时,两个对象的占用一块外部资源 第一个对象使用完成,调用析构函数释放资源,第二个对象使用完后,会再次去调用析构 函数,析构已经被释放的资源,将会造成二次释放。 SeqStack(const SeqStack& src) { _pstack=src._pastack; _top = src._top; _size = src._size; } */ //深拷贝:用户自定义拷贝构造函数 SeqStack(const SeqStack& src) { //为外部资源分配新的空间 _pstack = new int[src._size]; //拷贝 for (int i = 0; i <= src._top; ++i) { _pstack[i] = src._pstack[i]; } _top = src._top; _size = src._size; } private: int* _pstack;//动态开辟数组,存储顺序栈的元素 int _top;//指向栈顶元素的位置 int _size;//数组扩容后的总大小 };

拷贝构造函数的调用情景: 1.在一个对象以值传递的方式传入函数体时; 2.一个对象以值传递的方式从函数返回时; 3.一个对象用于给另外一个对象进行初始化(常称为赋值初始化)时

构造函数的初始化列表

其实我们在构造函数的函数体中对数据成员的赋值操作并不能准确的称之为给对象成员进行初始化,准确的应该称之为赋值操作,因为初始化只能初始化一次,二构造函数体内可以进行多次赋值。C++语法提供了一种给数据成员初始化的方法:初始化列表。 初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括 号中的初始值或表达式。

class CGoods { public: CGoods(const char* n, int a, double p) //初始化列表 :_amount(a)//相当于int amount = a; , _price(p)//#1构造函数的初始化列表 { strcpy(_name, n); /* //#2当前类类型构造函数体 _amount = a;//相当于int _amount;_amount=a; */ } private: char _name[20]; int _amount; double _price; };
注意
每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)类中包含引用成员变量、const成员变量,类类型成员(该类没有默认构造函数),必须放在初始化列表位置进行初始化。成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关。 class Test { public: Test(int data = 10) :mb(data), ma(mb) {} void show() { cout << "ma:" << ma << " mb:" << mb << endl; } private: //成员变量的初始化和它们定义的顺序有关,和构造函数初始化列表中出现的先后顺序无关。 //int ma;//此时ma将会被初始化为0xCCCCCCCC,因为在Windows VS下会给开辟的栈帧初始化为0xCCCCCCCC,即-858993460 int mb; int ma; };

析构函数

与构造函数功能相反,析构函数不是完成对象的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成类的一些资源清理工作。 语法:

~类名() { //函数体 }
析构函数的特征:
析构函数名是在类名前加上字符 ~。无参数无返回值。一个类有且只有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。对象生命周期结束时,销毁对象前,C++编译系统系统自动调用析构函数。对于自定义类型成员,析构函数会调用自定义类型成员的析构函数。对于自己显式开辟的资源(堆上开辟),需要自己显式定义一个析构函数,来去显式的释放资源,否则会造成内存泄漏。

赋值运算符重载函数

运算符重载

C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。

语法:

函数名字为:关键字operator后面接需要重载的运算符符号。 函数原型:返回值类型 operator操作符(参数列表) 例如:一个栈类的赋值运算符重载函数

SeqStack& operator=(const SeqStack& src) { //函数体 }
注意:
不能通过连接其他符号来创建新的操作符:比如operator@ 。重载操作符必须有一个类类型或者枚举类型的操作数 。用于内置类型的操作符,其含义不能改变,例如:内置的整型+,不 能改变其含义 。作为类员的重载函数时,其形参看起来比操作数数目少1,成员函数的操作符有一个默认的形参this,限定为第一个形参 。. * 、:: 、sizeof 、?: 、. 注意以上5个运算符不能重载。

赋值运算符重载函数

与上述成员函数一样,在用户没有显示定义赋值运算符重载函数时,编译器也会自动生成一个默认赋值运算符重载函数,完成字节序的拷贝。不过当类对象占有外部资源时,与拷贝构造函数一样,此时编译器自动生成的默认赋值运算符重载函数就会出现二次释放的问题。原因就是发生了浅拷贝,两个对象的外部资源占有同一块空间,在调用析构函数时就会发生二次释放的问题。 所以,当类对象占有外部资源时,我们就需要显示的定义一个赋值运算符重载函数。 例如:实现一个栈类的赋值运算符重载函数

class SeqStack { SeqStack& operator=(const SeqStack& src) { cout << "operator=" << endl; //防止自赋值 if (this == &src) return *this; //需要先释放当前对象占用的外部资源 delete[]_pstack; //开辟新的空间 _pstack = new int[src._size]; //赋值 for (int i = 0; i <= src._top; ++i) { _pstack[i] = src._pstack[i]; } _top = src._top; _size = src._size; return *this; } private: int* _pstack;//动态开辟数组,存储顺序栈的元素(外部资源) int _top;//指向栈顶元素的位置 int _size;//数组扩容后的总大小 };

正如代码中所注释的一样,我们在显示定义赋值运算符重载函数时应注意:

返回值:因为赋值运算符是支持连续赋值的,所以我们的返回值可以尽量不要为void,返回this指针,这样就可以实现连续赋值的操作。参数类型:参数类型应使用const修饰,防止修改右操作数,发生赋值错误。防止自赋值:赋值运算符的作用是将右操作数的值赋给左操作数,如果出现左右操作数相同(即自己给自己赋值)的情况,显然赋值运算符的操作是没有意义的,所以我们需要在赋值运算符重载函数中对自赋值操作检查。分配空间:如果左操作数的空间不足以支持右操作数进行赋值,我们需要先释放左操作数的外部资源空间,然后为其分配右操作数外部资源所占空间大小的空间。(此处一般不做检查,直接进行释放重新分配)进行赋值:在进行赋值操作时需要考虑是否会发生浅拷贝的问题。

取地址操作符与const取地址操作符重载函数

class Date { public: //取地址操作符重载函数 Date* operator&() { return this; } //const取地址操作符重载函数 const Date* operator&()const { return this; } private: int _year; // 年 int _month ; // 月 int _day ; // 日 };

这两个默认成员函数一般不用重载,编译器会默认生成符合我们需求的默认取地址操作符与const取地址操作符重载函数。 我们需要了解的就是这个编译器默认生成的函数中包含它们就可以了。

最新回复(0)