之前学习C++面向对象编程,只知道构造函数是创建实例对象时被自动调用;然后陆续看了一些书,知识点比较分散,现对构造函数进行一个全面总结,以便更加深入理解构造函数。
目录
-
构造函数是什么?
-
构造函数形式?
- 默认构造函数
- 构造函数初始化列表
1.构造函数是什么?
构造函数:类中通过一个或者多个特殊的成员函数来控制其对象初始化过程。构造函数的任务是初始化类中的数据成员,无论何时只要类的对象被调用,就会执行构造函数。并且,特殊在于构造函数与类名一致,无返回值。
1 | 一个或多个的成员函数 |
2 | 与类名一致,并且无返回值 |
3 | 类对象被调用,就会执行构造函数 |
4 | 任务是初始化类中的数据成员 |
2.构造函数形式?
默认的构造函数
我们在一个类中没有定义构造函数,创建了实例对象却能通过编译运行。这是为什么呢?哦,其实类通过一个特殊的构造函数来控制默认初始化过程,这个函数称之为默认构造函数,并且它无任何实参。
编译器创建的合成默认构造函数通过如下规则初始化类的数据成员:
类中存在初始值,通过初始值来初始化数据成员;
否则,默认初始化。
某些类不能依赖与默认的构造函数
- 只有当一个类中没有声明构造函数时,编译器才会自动生成默认的构造函数;
- 对于某些类,默认的构造函数可能会执行错误的行为。当一个类中定义了内置类型或者复合类型但是没有初始化,用户在创建类对象就可能得到得到未定义的值。(编译器不会报错,但是确实会引发未定义行为)
- 有时候编译器无法未某些类合成默认的构造函数。如:一个类中包含其他类型的成员且这个成员的类型没有默认的构造函数,在此情况下,必须自定义默认构造函数。
#ifndef SALE_DATA_H
#define SALE_DATA_H
using namespace std;
class Sale_data
{
public:
Sale_data() =default; //默认的构造函数
Sale_data(const std::string &s):bookNo(s){}
Sale_data(const std::string &s,unsigned n,double p):
bookNo(s),units_sold(n),revenue(p*n){}
Sale_data(std::istream &is)
{
read(is,*this);
}
std::string bookNo;
unsigned units_sold=0;
double revenue=0.0;
protected:
};
#endif
其中Sale_data()=default;是C++11新标准中要求编译器生成默认构造函数,可以出现在类内部或外部。当我们定义了其他的构造函数时,也必须要默认的构造函数。
一般情况下,都是在类内定义构造函数,但是我们仍能在类外部定义构造函数,只不过要分清楚在类外面的构造函数是哪一个类的成员。将上述代码中最后一个构造函数改成在类外,如下:
Sale_data(std::istream &is) { read(is,*this); //read函数的作用从is中, //读取一条交易信息存放在this对象中。 }
其中使用*this是将对象当成一个整体来访问,而不是直接访问对象的某个成员。
构造函数的初始化列表
如上程序所示,构造函数参数列表之后,函数体之前的部分就是构造函数的初始化列表。它的作用是为新创建的对象一个或多个数据成员初始化(不是赋值)。对于初始化列表中初始化与赋值之间的区别,以及数据成员初始化顺序。
-
初始化与赋值之间区别
string foo="Hello World"; //定义并初始化
string bar; //默认初始化一个空对象
bar="Hello World"; // 为bar赋一个新值
//对象的数据成员通过赋值形式初始化
Sale_data::Sale_data(const std::string &s,unsigned cnt,double price)
{
bookNo=s;
units_sold=cnt;
revenue=price*cnt;
}
有时候我们可以忽略初始化和赋值的区别,初始化操作时直接初始化数据成员;而赋值操作则是先初始化再赋值。其实还是存在很大的差异性,事关底层效率问题。我们都知道C++为底层而服务,最注重的就是效率问题,后者平白无故就多了一次次的赋值操作。
-
不能忽略的构造函数初始化列表
当成员有const,引用或者属于某种类类型,但是该类没有定义默认的构造函数,我们不得不使用成员初始化列表。如:
#include<iostream>
using namespace std;
class ConstRef
{
public:
ConstRef(int ii);
private:
int i;
const int ci; //const数据成员
int &ri; //引用
};
ConstRef::ConstRef(int ii)
{
i = ii;
ci = ii;
ri = i;
}
//ConstRef::ConstRef(int ii) :i(ii), ci(ii), ri(ii) {}
int main(void)
{
ConstRef ref(2);
return 0;
}
我们都知道const常量和引用必须要直接初始化,通过赋值操作将会引发错误,编译出现的错误如下:1>g:\dev\ctor\ctor error C2789: “ConstRef::ci”: 必须初始化常量限定类型的对象
C2530: “ConstRef::ri”: 必须初始化引用
C2166: 左值指定 const 对象
而使用构造函数初始化列表形式则不会出现错误,因为这种形式本来就是直接初始化。
因此,我们必须养成对构造函数数据成员列表初始化的好习惯,一是为了避免不必要的错误,而是为了C++的高效性!
-
数据成员初始化顺序
#include<iostream>
using namespace std;
class X
{
int i; //声明顺序i,j
int j;
public:
X(int val) :j(val), i(j) {} //列表初始化形式上好像是
//用val初始化j,然后用j初始化i;
void print()
{
cout << i << "," << j << endl;
}
};
int main(void)
{
X x(3);
x.print();
return 0;
}
通过成员初始化列表在形式上看起来好像是列表初始化形式上好像是 用val初始化j,然后用j初始化i;然而类中数据成员的初始化顺序只与数据成员的声明有关,而与成员初始化列表无关,因此正确的打开方式是首先用未初始化的j(垃圾值)初始化i,再用val初始化j,程序运行的结果为:-858993460,3。