1. C++构造函数:
C++中构造函数(constructor)的作用是用来控制类对象的初始化过程。构造函数的作用是初始化类对象的数据成员,无论何时只要类的对象被创建,就会执行构造函数。
1.1 默认构造函数:
当定义一个类对象时,如果没有为对象提供初始值,则此时会执行默认初始化,当执行默认初始化时将调用类的 “默认构造函数”(default constructor)。 默认构造函数无需任何实参,形式如下:
class Sales_data {
public:
Sales_data() {} //默认构造函数 (无需任何实参)
private:
std::string bookNo;
unsigned int units_sold = 0;
double revenue = 0;
};
1.2 合成的默认构造函数:
编译器自动创建的默认构造函数 称为 “合成的默认构造函数”(synthesized default constructor)。
合成的默认构造函数将按照如下规则初始化类的数据成员:
(1)如果存在类内初始值,则用初始值来初始化成员;
(2)否则,默认初始化成员。
(例如 Sales_data 例子中的 units_sold 和 revenue 数据成员都提供了初始值,而 revenue 没有提供初始值)
注意:
不要过度依赖编译器提供的合成的默认构造函数,原因如下:
(1)一旦类中定义了任一类型的其他的构造函数,则编译器将不会生成 合成的默认构造函数,此时如果我们又没有显式的定义一个默认构造函数,将会造成类没有默认构造函数;
(这种情况在编译时将会报错:“no matching constructor for initialization”)
(2)对于某些类来说,合成的默认构造函数可能会执行错误的操作,例如对于局部变量的默认初始化,其值是未定义的;
(3)有些时候编译器无法为某些类生成合成的默认构造函数,例如某些类中包含没有默认构造函数的成员。
=default 声明符:
如果我们定义了其他形式的构造函数,而又需要编译器为我们生成合成的默认构造函数时,可以使用 =default:
class Sales_data {
public:
Sales_data() = default; //编译器会生成合成的默认构造函数
Sales_data(unsigned int sold) : units_sold(sold) {}
private:
std::string bookNo;
unsigned int units_sold = 0;
double revenue = 0;
};
另外一点需要注意,C++类中不能同时共存默认构造函数和全部指定数据成员默认值的构造函数。例如下面的例子中编译时就会出错:
class Sales_Item {
public:
Sales_Item() {} //默认构造函数
Sales_Item(int x = 1, int y = 2) : a(x), b(y) {} //所有数据成员都指定初始值的构造函数
private:
int a;
int b;
};
------
//编译时会出现二义性错误:
error: call to constructor of 'Sales_Item' is ambiguous
1.3 隐式的类类型转换:
如果构造函数 只接受一个实参,则它实际上定义了转换为此类类型的隐式转换机制,这种构造函数被称作为 “转换构造函数”(converting constructor) 。
例如:
class Sales_data {
public:
Sales_data() = default;
Sales_data(std::string s, unsigned cnt, double price) : bookNo(s), units_sold(cnt), revenue(cnt*price) {}
Sales_data(std::string s) : bookNo(s) { cout << "func constructor" << endl; }
void combine(const Sales_data& item) { cout << "func combine" << endl; }
private:
std::string bookNo;
unsigned int units_sold;
double revenue;
};
int main() {
std::string null_book = "9-999-99999-9";
Sales_data item;
item.combine(null_book);
//此时发生隐式转换:string类型的null_book隐式转换为Sales_data类型
//!!编译器会使用给定的string对象自动创建一个Sales_data类型的【临时对象】
return 0;
}
运行结果:
func constructor //先调用Sales_data(string)这个构造函数 通过 null_data参数构造一个Sales_data类型的临时对象
func combine //将上面生成的临时对象传入给combine()函数
1.3.1 关于隐式转换需要注意的几点:
① 注意在上面的例子中,当发生隐式转换时,编译器会使用给定的string对象自动创建一个Sales_data类型的 临时对象,而临时对象只能绑定到const类型的数据上(因为临时对象的值不能被修改),所以如果需要隐式转换,combine()函数的形参必行为const类型,如果没有const关键字,编译时将会报错:
error: non-const lvalue reference to type 'Sales_data' cannot bind to a value of unrelated type 'std::__1::string'
② 另外需要的是,只允许一步类型类类型转换。
1.3.2 explicit 关键禁止隐式类型转换:
当需要明确禁止类发生类型转换时,可通过将构造函数声明为explicit加以阻止。
class Sales_data {
public:
Sales_data() = default;
Sales_data(std::string s, unsigned cnt, double price) : bookNo(s), units_sold(cnt), revenue(cnt*price) {}
//使用explicit禁止使用此构造函数发生隐式的类型转换:
explicit Sales_data(std::string s) : bookNo(s) {}
void combine(const Sales_data& item) {}
};
1.3.3 关于explicit关键字的注意事项:
① 关键字explicit只对一个实参的构造函数有效,需要多个实参的构造函数不能用于执行隐式转换,所以无须将这些构造函数指定为explicit的;
② 只能在类内 声明 构造函数时使用explicit关键字,在类外定义时不应重复;
③ explicit构造函数只能用于直接初始化:
Sales_data item1(null_book); //正确:直接初始化
Sales_data item2 = null_book; //错误:不能将explicit构造函数用于拷贝形式的初始过程
④ 声明为explicit的构造函数不能用于隐式类型转换,但可以用于显式的强制类型转换:
//正确:实参是一个显式构造的Sales_data对象
item.combine(Sales_data(null_book));
item.combine(static_cast<Sales_data>(null_book));
1.3.4 小结:关于隐式转换 和 explicit关键字:
对于类类型的隐式类型转换,其本质就是调用类中的某个只含有一个实参的构造函数,通过入参构造一个临时的类类型对象。
所以explicit关键字禁止隐式转换的机理就是限制这个可能被用于构造临时对象的构造函数,使其不能被隐式调用。
1.4 拷贝构造函数:
如果一个构造函数的第一个参数是自身类类型的引用,且任何额外的参数都有默认值,则此构造函数是 “拷贝构造函数”(copy constructor)。
class Foo {
public:
Foo();
Foo(const Foo& );
};
① 拷贝构造函数的第一个参数必须是引用类型;
② 虽然我们可以定义一个接受非const引用的拷贝构造函数,但此参数几乎总是一个const的引用;
③ 拷贝构造函数通常不应该是explicit的,因为拷贝构造函数几种情况下都会被隐式地使用;
④ 如果类型没有显式的定义拷贝构造函数,则编译器会为我们生成一个 “合成的拷贝构造函数”(synthesized copy constructor)。
class Sales_data {
public:
Sales_data(const Sales_data& orig);
private:
std::string bookNo;
int units_sold;
double revenue = 0.0;
};
Sales_data::Sales_data(const Sales_data& orig) : bookNo(orig.bookNo), units_sold(orig.units_sold), revenue(orig.revenue) {}
1.4.1 “拷贝初始化” 与 “直接初始化” 的区别:
//以一个string类型的变量的初始化为例:
string dots(10, '0'); //直接初始化
string s(dots); //直接初始化
string s2 = dots; //拷贝初始化
string null_book = "9-999-99-9"; //拷贝初始化
string nines = string(100, '9'); //拷贝初始化
当使用 直接初始化 时,我们实际上是要求编译器使用普通的函数匹配来选择与我们提供的参数最匹配的构造函数(可能是默认构造函数,可能是带参数的构造函数);
当使用 拷贝初始化 时,我们要求编译器将右侧运算对象拷贝到正在创建的对象中,如果需要的话还要进行类型转换。
1.5 拷贝赋值运算符:
拷贝赋值运算符实质上是对“=”运算符的重载,而运算符的重载本质上是函数。
class Foo {
public:
Foo& operator=(const Foo& orig) { }
};
为了与内置类型的赋值保持一致,赋值运算符通常会返回一个指向其左侧运算对象的引用。
与处理拷贝构造函数一样,如果一个类未定义自己的拷贝赋值运算符,编译器会为它生成一个“合成的拷贝赋值运算符”(synthesized-assignment operator)。
2. C++会隐式的生成哪些构造函数:
编译器可以隐式的为类创建 default默认构造函数、copy拷贝构造函数、copy assignment赋值操作符、析构函数。
其中,合成的构造函数和析构函数,主要供编译器用来放置一些必要的代码,如对成员变量(非静态)的默认构造函数、放置基类的构造函数(如果此类是一个派生类) 等;析构函数则反之(合成的析构函数是非virtual)。
至于 拷贝构造函数和拷贝赋值运算符,编译器合成的版本只是简单复制(浅拷贝)。
有两种情况,编译器会拒绝生成合成的拷贝赋值运算符:
① 赋值操作不合法:
例如:
class NameObject {
public:
//NameObject() = default;
NameObject(string &name) : nameValue(name) {}
private:
string& nameValue;
};
对于NameObject类,编译器将拒绝生成合成的拷贝赋值运算符,程序可以编译通过,但类中没有拷贝赋值运算符,当使用此类型对象进行拷贝赋值操作时,编译器将会报错:
error: object of type 'NameObject' cannot be assigned
because its copy assignment operator is implicitly deleted
实际上,对于NameObject类,编译器同样也会拒绝为其生成default默认构造函数。如果我们要求编译器为我们合成默认构造函数,编译时将会报错:
warning: explicitly defaulted default constructor is
implicitly deleted
② 当基类中的 “operator=” 是private,则派生类的“operator=” 编译器将拒绝合成。
3. 如何禁止编译器自动生成构造函数:
3.1 方法一:将构造函数声明为private:
如果不想让你声明的类类型的对象被拷贝或赋值,则需要禁止编译器生成合成的拷贝构造函数和拷贝赋值运算符。
有一种方法可以禁止编译器生成合成的构造函数:
由于编译器生成的构造函数都是public,所以为了阻止这些函数被创建出来,我们可以将拷贝构造函数或拷贝赋值运算符声明为private,而不定义它们。
例如:
class HomeForSale {
public:
...
private:
HomeForSale(const HomeForSale&);
HomeForSale& operator=(const HomeForSale&);
};
3.2 方法二:使用 “=delete” 声明符 :
在C++11中,引入了 “=delete” 声明符,专门用于明确禁止不要生成合成的构造函数。例如:
class HomeForSale {
public:
HomeForSale(const HomeForSale&) = delete;
HomeForSale& operator=(const HomeForSale&) = delete;
private:
};
4. 关于构造函数的一些注意事项:
4.1 别让异常逃离析构函数:
(1)析构函数绝对不要吐出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕获任何异常,然后吞下它们(不传播)或结束程序。
(2)如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么 class 应该提供一个普通函数(而非在析构函数中)执行该操作。
4.2 不要在构造和析构过程中调用virtual函数:
在构造和析构期间不要调用virtual函数,因为这类调用从不下降至derived class(比起当前执行构造函数和析构函数的那层)。
4.3 令operator= 返回一个 reference to *this
令“operator=”赋值运算符返回一个“引用类型”只是一种约定俗成的做法,并无强制性。 如果不遵循它,而是改为返回“值结果”,代码一样可以编译通过 且正常运行。
然而这份协议被所有内置类型和标准程序库(STL)提供的类型如 string、vector 等 共同遵守。因此一般在自定义类型时也会遵守这一协议,即令 拷贝赋值运算符operator= 返回一个引用类型。
回顾拷贝赋值运算符的写法:
class Base {
public:
Base& operator=(const Base& rhs) {
this->num = rhs.num;
return *this;
}
private:
int num;
};
拷贝赋值运算符的本质上是对“=”运算符的重载,对于类对象的“+”加法运算的重载,写法为:
//1. 当 operator+ 是非成员函数 :
Base operator+(const Base& a, const Base& b) {
return Base(a.num + b.num);
}
//此时不能返回Base&,因为temp是函数内的局部变量,退出作用域后将被销毁
//2. 当 operator+ 是成员函数:
class Base {
public:
Base operator+(const Base& rhs) {
return Base(this->num + rhs.num);
}
private:
int num;
};
4.4 在operator= 中处理“自我赋值”:
(1)确保当对象自我赋值时operator= 有良好行为。其中技术包括比较“来源对象”和“目标对象”的地址、精心周到的语句顺序、以及copy-and-swap。
(2)确定任何函数如果操作一个以上的对象,而其中多个对象是同一个对象时,其行为仍然正确。
4.5 复制对象时勿忘其每一个成分:
(1)Coping函数应该确保复制“对象内的所有成员变量”及“所有base class成分”。
(2)不要尝试以某个coping函数实现另一个coping函数。应该将共同机能放进第三个函数中,并由两个coping函数共同调用。
第一点:对于自行编写的 copy constructor、 copy assignment,应该保证所有的数据成员都被拷贝赋值。但是,如果遗漏了某个成员没有被拷贝赋值,此时编译器不会报错,只能靠程序员自行保证。
特别是对一个类新增了成员时,一定要记得更新对应的拷贝构造函数和拷贝赋值运算符。
第二点:对于有继承关系的拷贝构造,派生类中的拷贝构造函数不会自动调用基类的拷贝构造函数,如果你没有在派生类拷贝构造函数的初始值列表中显式的调用基类的拷贝构造函数,则编译器会调用基类的default默认构造函数,这时的初始值可能并非预期。
当在派生类的拷贝构造函数中 调用 基类的拷贝构造函数时,直接传入 派生类类型的引用即可,基类会将其阶段。
例如:
class Base {
public:
Base(const Base& rhs) : num(rhs.num) {}
private:
int num;
};
class Derived : public Base {
public:
Derived(const Derived& rhs) : Base(rhs), len(rhs.len) {}
private:
int len;
};