C++中有六大默认成员函数:
构造函数
构造函数的作用
- 构建对象
- 初始化
- 类型转换(单参才能转换)
构造函数的特点
关于第7点可能有点模糊,举个例子:
- 没有返回值,并且不能指定返回值类型
- 函数名与类名相同
- 创建对象是由编译器自动调用(在对象生命周期内只会调用一次)
- 可以重载
- 如果没有显示定义,编译器会默认合成一个缺省的构造函数(在有必要的情况下:如类成员变量中有类对象,且该类对象有自己的构造函数)
- 也可以放在类外定义(像类成员函数一样)
- 缺省构造函数一般只能有一个:1.无参的; 2.全缺省的。
- 不能声明为const
class Date { public: Date() { cout << "Date::Date() call" << endl; } Date(int a = 1, int b = 2) { cout << "Date::Date_int_int() call" << endl; } private: int _year; int _month; int day; }; int main() { //Date d1; //编译出错 Date d2(1, 2); //不影响 (同普通函数重载类似,关键在于调用时是否存在二义性问题。若存在,则编译报错) Date d6(); //函数声明 return 0; }
8.详解class Sales_item {
public:
Sales_item() const; //error
};
const 构造函数是不必要的。创建类类型的 const 对象时,运行一个普通
构造函数来初始化该 const 对象。构造函数的工作是初始化对象。不管对象是
否为 const,都用一个构造函数来初始化化该对象
构造函数的初始化列表
//Date类构造函数 Date(int year = 2016, int month = 1, int day = 1) : _year(year) //实际初始化顺序按类中成员变量排列顺序进行初始化,与这里的显示顺序无关 , _month(_year) //随意这个顺序最好也按类中成员变量排列顺序来 , _day(_month) { cout << "Date::Date_int_int() call" << endl; /*_year = year; _month = month; _day = day;*/ }
如果类中包含:
- 非静态const数据成员(即没有static修饰的)
- 引用
- 没有缺省构造函数的类成员变量
则需要在初始化列表进行初始化。
对这里的第3点再来举个例子:
class Date { public: Date(int year) : _year(year) { cout << "Date::Date() call" << endl; } private: int _year; int _month; int _day; }; int main() { Date d1 = 1; //编译器将1隐式类型转换为Date类型的对象(将1作为参数传递给构造函数) return 0; // 隐式类型转换后为无名对象,编译器进行了优化,并没有调用拷贝构造函数,而是直接将这个无名对象作为了d1 //类似于函数中返回一个无名对象 return Date(); }
如下依旧做了优化:
class Date { public: explicit Date(int year) //此关键字抑制隐式类型转换 : _year(year) { cout << "Date::Date() call" << endl; } Date(const Date &d) { cout << "Date::Date_copy() call" << endl; _year = d._year; _month = d._month; _day = d._day; } private: int _year; int _month; int _day; }; int main() { Date d1 = (Date)1; //显示转换 Date d2 = d1; Date d3(d2); return 0; }
通常,除非有明显的理由想要定义隐式转换,否则,单形参构造函数应该为 explicit。将构造函数设置为explicit 可以避免错误,并且当转换有用时,用户可以显式地构造对象。
类通常应定义一个默认构造函数
在某些情况下,默认构造函数是由编译器隐式应用的。如果类没有默认构造函数,则该类就不能用在这些环境中。为了例示需要默认构造函数的情况,假定有一个 NoDefault 类,它没有定义自己的默认构造函数,却有一个接受一个string 实参的构造函数。因为该类定义了一个构造函数,因此编译器将不合成默认构造函数。NoDefault 没有默认构造函数,意味着:
1. 具有 NoDefault 成员的每个类的每个构造函数,必须通过传递一个初始的 string 值给 NoDefault 构造函数来显式地初始化 NoDefault 成员。
2. 编译器将不会为具有 NoDefault 类型成员的类合成默认构造函数。如果这样的类希望提供默认构造函数,就必须显式地定义,并且默认构造函数必须显式地初始化其 NoDefault 成员。
3. NoDefault 类型不能用作动态分配数组的元素类型。
4. NoDefault 类型的静态分配数组必须为每个元素提供一个显式的初始化式。
5. 如果有一个保存 NoDefault 对象的容器,例如 vector,就不能使用接受容器大小而没有同时提供一个元素初始化式的构造函数。实际上,如果定义了其他构造函数,则提供一个默认构造函数几乎总是对的。通常,在默认构造函数中给成员提供的初始值应该指出该对象是“空”的。
拷贝构造函数
拷贝构造函数的特点
- 一种特殊的构造函数
- 只有单个形参:本类类对象的引用(一般为常引用,因为实参的值不需要被改变)
- 若没有显示定义,则合成一个默认的拷贝构造函数
- 用对象构建对象时调用(需要产生一份拷贝)(用临时对象构建对象时编译器会做优化,不调用拷贝构造函数,而是调用构造函数)
例:
Date(const Date &d)2.详解:参数为什么一定要为引用?
{
_year = d._year;
_month = d._month;
_day = d._day;
}
如果参数为Date d,那么实参传递给形参的时候,将调用拷贝构造函数(要生成一份参数的拷贝);调用拷贝构造函数就又要传参,传参又要调用拷贝构造函数,如此递归下去,栈终将溢出!
4.详解:关注构造函数中最后的例子。
涉及到浅拷贝与深拷贝:如果类成员变量中有指针,那么默认的拷贝构造函数将只赋值指针的值,而不复制指针所指向内存空间的内容(浅拷贝),所以实际上两个对象中的这个变量都指向同一块内存,对其中一个指针指向的内存修改,另一个也会改变,不安全。同理,赋值运算符最好也重载。为什么编译器会合成默认的拷贝构造函数还要重载拷贝构造函数?
析构函数
析构函数的特点
- 在对象销毁时,由编译器自动调用
- ~类名(){}
- 没返回值
- 没参数(不能重载)
- 若没有显示定义,则编译器默认合成一个
- 析构函数体内并不是删除对象,而是做一些清理工作
例:
~Date(){}对象在栈上分配,栈后进先出。所以同一作用域的对象,先构造的后析构.
运算符重载
特点
- 参数至少有一个类类型(或自定义类型)
- 运算符重载后不保证顺序,尤其是&&,||
- == 和 !=一般成对重载
- 重载后置++,--多一个int型参数,用以区分前置++,--,调用时由编译器自动传参
当内置操作符和类型上的操作存在逻辑对应关系时,操作符重载最有用。使用重载操作符而不是创造命名操作,可以令程序更自然、更直观,而滥用操作符重载使得我们的类难以理解。
选择成员或非成员实现
为类设计重载操作符的时候,必须选择是将操作符设置为类成员还是普通非成员函数。在某些情况下,程序员没有选择,操作符必须是成员;在另一些情况下,有些经验原则可指导我们做出决定。下面是一些指导原则,有助于决定将操作符设置为类成员还是普通非成员函数:
1.赋值(=)、下标([])、调用(())和成员访问箭头(->)等操作符必须定义为成员,将这些操作符定义为非成员函数将在编译时标记为错误。
2.像赋值一样,复合赋值操作符通常应定义为类的成员,与赋值不同的是,不一定非得这样做,如果定义非成员复合赋值操作符,不会出现编译错误。
3.改变对象状态或与给定类型紧密联系的其他一些操作符,如自增、自减和解引用,通常就定义为类成员。
4.对称的操作符,如算术操作符、相等操作符、关系操作符和位操作符,最好定义为普通非成员函数。文章中所有例子源码与重载操作符相关例子:
Date.cpp: https://github.com/Fireplusplus/Code_Cpp/blob/master/Date.cpp
C++类默认成员函数与重载
最新推荐文章于 2021-12-28 23:03:40 发布