C++中的构造函数详解

目录
  • 普通变量的初始化
  • 构造函数
  • 一定会生成默认构造函数吗?
  • 防止隐式类型转换
  • 赋值与初始化的区别
  • 对象的计数
  • 成员初始化的顺序
  • 类的引用成员
  • 构造函数使用注意事项
  • 参考
  • 总结

普通变量的初始化

当我们在定义一个变量不给它指定一个初始值时,这对于全局变量和局部变量来说结果会不一样。全局变量在程序装入内存时 就已经分配好空间,程序运行期间其地址不变,它会被初始化为全0(变量的每一位都为0)。但是局部变量定义在函数内部,存储在栈上,当函数被调用时,栈会分配一部分空间来存储该局部变量(也就是只分配空间,而不管赋值),这时此空间的值是已经有的,它是一个随机的值,如果程序员不对它进行初始化,将会产生不好的后果(如果将栈空间内存的变量初始化工作交给编译器,则每次函数调用都会被重新赋值,则会大大增加开销)。

示例全局变量,这些对象会被赋一个默认的初始值。如图:

构造函数

对象和我们前面提到的基本类型的变量一样,定义的时候也能够被初始化。而这些初始化操作会比基本数据类型初始化更复杂一些,不仅涉及对类内基本数据类型的初始化,还包括进行动态内存分配,打开文件等操作。这时就需要为类设计一个构造函数来专门负责这些操作,对象一旦创建就立即调用它,对其进行强制初始化(这是强制执行的)。

构造函数属于成员函数比较特殊的一种(与其他成员函数不同,它不能被显式的调用,所以也没有必要为它设置返回值),它在对象的初始化时起作用(设计类时往往将初始化功能在构造函数中实现),我们希望创建出的所有变量都能被赋予有意义的能力,而不是未知的,可以在创建之后再次改变他,但在此之前若要使用它,将会发生意外,所以在制度上我们就要对其进行严防死守(使其被创建后一定处于良好状态)。

构造函数可以重载,可以编写多个构造函数,参数列表不同。生成对象时,将会根据参数列表的参数信息决定该调用哪个构造函数。如果没有对参数有任何交代,编译器就认为应该调用无参构造函数。

当在局部空间或全局区创建自定义类型的对象时,构造函数会被自动调用(设计好初始化后,每当创建对象就会自动调用构造函数会使每一个对象都处于良好的随时可用的状态)。如图:

以上演示是在类的外部对构造函数进行了定义,实际开发中,一般将类的定义部分放到头文件中(.h),而类内成员函数的实现放到实现文件中(.cc)中。这对我们在不改变类的定义的情况下修改他的成员函数提供了方便。这比在类内直接实现有一个好处,就是不需要每次改变函数体都要重新编译(前题是不改变函数声明)。

明确一点,如果在类内对成员函数进行了实现,它将会被隐式地声明为内联函数(由编译器决定是否最终把它变为内联函数)。

一般超过5行代码的函数就没有必要设为内联函数了(函数内限制在两个表达式最好)。

在我们只定义了有参的构造函数后,就不会有无参的构造函数了,需要我们自己定义一个无参的构造函数。

class MyArray
{
	char array_;
public:
	MyArray(char a_new_array='x'):array_(a_new_array);//给出默认参数
	MyArray();
	void PrintMyArray();
};
//类实现省略
void test()
{
	MyArray a_long_array('c');//调用有参的构造方法
	MyArray a_short_array();//调用无参的构造方法
}

初始化方法一般给出形参列表,在这里对类内成员赋初值(此时构造函数的函数体还未开始执行),我们这里是对其输入了一个字符。构造函数还可以调用其他类的构造函数来初始化对象中的成员变量(这是编译器自动调用的,如在隐式的数据类型转换时)。当然也可以在构造函数函数体内为成员变量赋值(这并不是初始化,执行函数体后赋值之前,成员变量已经有一个无意义的值了)。

MyArray::MyArray(char init_array)
{//进入构造函数体后成员变量已经有值了!
	array_=init_array;//在函数体内进行赋值
	cout << "MyArray的构造方法执行了!\n";
}

形参列表具有立即性(在成员变量被创建时形参列表的值就立即为其初始化),而构造函数体内需要执行赋值语句。对于一些自定义类型的成员变量进行初始化,形参列表初始化往往比赋值语句效率更高。此外对于一些特殊的成员只能采用形参列表的方式进行初始化。

一定会生成默认构造函数吗?

前面我们说过,当我们没有编写构造函数时,编译器会为我们生成一个默认的构造函数,对于初学者来说,这就够了,但现在我们要强调几点。

1.如果类中我们没有定义构造函数并且类中成员变量含有自定义类型成员变量时,编译器会生成默认构造函数,并且调用自定义类型的构造函数。

2.当父类具有默认构造函数而子类没有任何构造函数,当生成子类对象时,会生成子类的默认构造函数,在这个函数中调用父类默认构造函数。

3.当一个含有虚函数并且没有构造函数时编译器会生成默认的构造函数。

4.出现菱形继承时,两个父类分别进行虚继承,当出现子类对象时,子类会生成一个自己的默认构造函数,在构造函数内调用两个父类的构造函数。

5.在定义类时,直接为成员变量赋值,当生成对象时,会调用默认构造函数。

防止隐式类型转换

如果构造函数只有一个参数的话最好将此构造函数声明为ecplicit,他能防止编译器将explicit的构造函数进行隐式类型转换。

class MyArray
{
	char array_;
	double array_length_;
public:
	MyArray(double init);
	MyArray(char init);
	void PrintMyArray(MyArray a_array);
};
void MyArray::PrintMyArray(MyArray a_array)
{//理应接受一个对象,有时会接受一个基本类型
//此时会进行隐式类型转换
	cout << "MyArray为:"<< a_array.array_length_ << "\n";
}
void test()
{
	MyArray first_array(3.0);
	first_array.PrintMyArray(5.0);//隐式的将5.0转化为了MyArray对象
}

修改如下:

class MyArray
{
	char array_;
	double array_length_;
public:
	explicit MyArray(double init);
	explicit MyArray(char init);
	void PrintMyArray(MyArray a_array);
};

此时调用first_array.PrintMyArray(5.0)就会报错了。

赋值与初始化的区别

必须明确初始化和赋值的概念,当一个对象刚被创建时,对它的赋值操作为初始化,而赋值是修改一个已经存在并且有值的对象。

int a_int=1;//初始化,a_int被创建出来并被立即初始化
a_int=2;//赋值,a_int已经存在,没有新的变量被创建出来

初始化出现在构造函数中,而赋值出现在operator=操作符中。

一个对象只能被初始化一次,而赋值却可以不限制次数,在对象的声明期内都可以进行。而且进行赋值时可能会进行类型转换(此时会产生临时对象)。

一个构造函数的初始化可以借用另一个构造函数,他被称为委托构造函数。

class MyArray
{
	char array_;
	double array_length_{ 1.0 };
public:
	MyArray(double init, double in, double it);
	MyArray(double init);
	MyArray();
	double GetMyArrayLength();
	bool CompareMyArrayLength(MyArray a_array);
	void PrintMyArray(MyArray a_array);
};
MyArray::MyArray(double init, double in, double it)
{
	cout << "构造函数1执行了!" << endl;
}
MyArray::MyArray(double init) :MyArray{ init,init,init }
{
	cout << "构造函数2执行了!" << endl;
}
//其他函数实现略
void test()
{
	MyArray first_array{ 1.0,1.1,1.2 };
	MyArray second_array{ 2.0 };
}

输出如下:

在执行构造函数2时先利用构造函数1进行初始化操作。

对象的计数

有时我们希望知道我们定义的一个类有多少个对象,就可以这样操作:

class MyArray
{
	static int sum_;
public:
	MyArray();
	~MyArray();
	static int GetMyArraySum() { return sum_; }
};
int MyArray::sum_ = 0;
MyArray::MyArray()
{
	sum_++;
}
MyArray::~MyArray()
{
	sum_--;
}
void test()
{
	MyArray first_array;
	MyArray second_array;
	MyArray forth_array;
	cout << "MyArray有" << MyArray::GetMyArraySum() << "个对象" << endl;
}

输出如下:

因为创建对象的时候构造函数一定会被调用,所以它能准确的记录当前的对象数。

成员初始化的顺序

一个类中成员的初始化顺序和它们在类中被声明的顺序是一致的。编译器会忽略构造函数中的顺序,这是因为对象的析构需要和对象的成员构造顺序相反的要求 。

所以在编程中,成员变量的初始化顺序应与类定义中的声明顺序保持一致。

class Employee
{//错误示范
	string email_,first_name_,last_name_;
public:
	Employee(const char* first_name,const char* last_name):first_name_(first_name),last_name_(last_name),email_(first_name_+last_name_+"@163.com"){}

类中定义的email_是在first_和last_之前的,但程序却用其他未初始化的成员变量来为它初始化。

类的引用成员

如果类中的某个费静态数据成员是一个引用的话,所有的引用必须被明确地初始化,所以在构造函数中都要显示初始化。

class MyArray
{
	YourArray& a_array_;
public:
	MyArray(YourArray&);
}
MyArray::MyArray(YourArray& init)//会被报错
{
}

这个成员引用有两个特点,一,在创建a_array_时就要为它绑定一个对象,二,一旦绑定后,就不能再绑定其他变量了。

构造函数使用注意事项

1.除非必要不要在构造函数内做与初始化对象无关的事情,减少它的功能可以使每个函数功能更加明确,增加效率。

2.类的非静态const成员和引用成员只能在构造函数的初始化列表中进行初始化,这是由const和引用自身特点决定的。

3.不能同时定义一个无参的构造函数和一个参数全部为默认值得构造函数。

4.拷贝构造函数的参数应为引用传递,如果为传值则会与有一个参数的构造函数冲突。

参考

The C++ Programming Language (美) Bjarne Stroustrup

cpp参考:https://zh.cppreference.com

总结

本篇文章就到这里了,希望能够给你带来帮助,也希望您能够多多关注我们的更多内容!

(0)

相关推荐

  • C++基于OpenCV实现手势识别的源码

    先给大家上效果图: 源码在下面 使用 RGB 值分割手部区域,即手部的 GB 值将与背景不同 或者使用边缘检测 或者 背景减法. 我这里使用了背景减法模型.OpenCV为我们提供了不同的背景减法模型,codebook   它的作用是对某些帧进行一段时间的精确校准.其中对于它获取的所有图像:它计算每个像素的平均值和偏差,并相应地指定框. 在前景中它就像一个黑白图像,只有手是白色的 用 Convex Hull 来找到指尖.Convex hull 基本上是包围手部区域的凸集. 包围手的红线是凸包.基本

  • C++类的特种函数生成机制详解

    目录 C++类的特种函数生成机制 规则 例子:A BUG 例子:std::mutex和std::thread 题外话:为什么std::mutex不可移动? 总结 C++类的特种函数生成机制 规则 参考Effective Morder C++上的说明: 默认构造函数:仅当类中不包含用户声明的构造函数时才生成. 析构函数:默认生成,当基类的析构函数为虚时,派生类的默认析构函数为虚函数. 拷贝构造函数:仅当类中不包含用户声明的拷贝构造函数时才生成.如果该类声明了移动操作,那么拷贝构造函数将被定义为删除

  • 用C++的odeint库求解微分方程

    目录 1.集成方程 2.求解单摆模型 2.1 微分方程标准化 2.2 代码实现 微分方程的标准形式为: 即:\dot{\boldsymbol{x}} = \boldsymbol{f}(\boldsymbol{x}, t),\, \boldsymbol{x}(0) = \boldsymbol{x_0} 这是一阶微分方程组, \boldsymbol{x} 和 \boldsymbol{f}(\boldsymbol{x}, t) 均为向量.如果要求解高阶微分方程,需要先转换成一阶微分方程组后再用odei

  • C++的命名空间详解

    目录 C++ | C++命名空间 C++命名空间 定义命名空间 实例1: using 指令 实例2: 实例3: 不连续的命名空间 嵌套的命名空间 实例4: 实例5: 笔记: 实例6: 实例7: 总结 C++ | C++命名空间 C++命名空间 一个中大型软件往往由多名程序员共同开发,会使用大量的变量和函数,不可避免地会出现变量或函数的命名冲突. 当所有人的代码都测试通过,没有问题时,将它们结合到一起就有可能会出现命名冲突. 例如小李和小韩都参与了一个文件管理系统的开发,它们都定义了一个全局变量

  • 浅谈C++ 设计模式的基本原则

    先上银行类案例代码如下: #include<iostream> using namespace std; class BankWorker { public: void save() { cout << "存款" << endl; } void moveM() { cout << "取款" << endl; } void jiaofei() { cout << "缴费" &l

  • C++实现矩阵对称正交化的示例代码

    1.python代码 import numpy as np import pandas as pd df=pd.DataFrame() df['fac_01']=(34, 45, 65) df['fac_02']=(56, 25, 94) print(df) print('------------------矩阵的特征跟D.和特征向量U-----------------------') D,U=np.linalg.eig(np.dot(df.T, df)) # 求矩阵的特征跟D.和特征向量U p

  • C++ 标准模板类详解

    目录 1 标准模板库 2.泛型编程 总结 1 标准模板库 STL提供了表示容器.迭代器.函数对象和算法的模板. 容器:类似数组存储若干值,切实同质的: 迭代器:遍历容器的对象,类似遍历数组的指针,广义指针: 算法:完成特定的任务: 函数对象:类对象或函数指针. 模板类 vector erase() 删除矢量中给定区间元素.接受两个迭代器参数(该参数定义了要删除的区间),迭代器1指向区间起始处,迭代器2指向区间终止处的后一个位置. // delete first and second elemen

  • C++的异常处理一篇带你了解

    目录 一.背景 二.C++ 异常处理 三.抛出异常与捕获异常 四.catch(...)的作用 总结 一.背景 程序运行时常会碰到一些异常情况,例如: 做除法的时候除数为 0: 用 new 运算符动态分配空间时,空间不够导致无法分配: 访问数组元素时,下标越界:打开文件读取时,文件不存在. 这些异常情况,如果不能发现并加以处理,很可能会导致程序崩溃. 所谓"处理",可以是给出错误提示信息,然后让程序沿一条不会出错的路径继续执行:也可能是不得不结束程序,但在结束前做一些必要的工作,如将内存

  • C++实现广度优先遍历图

    本文实例为大家分享了C++实现广度优先遍历图的具体代码,供大家参考,具体内容如下 广度优先遍历 void bfs(int start, int parent[], int dist[], int seen[], int visited[]) { std::queue <int> q;//建立数据队列q int v; q.push(start); //让开始序列入栈 parent[start] = start; // 开始节点的父节点是开始节点 dist[start] = 0; // 初始化距离

  • C++实现对象化的矩阵相乘小程序

    复习数学1的线性代数,矩阵相乘这块有点晕,想编个C++对象化的矩阵相乘小程序. 相乘部分 void sum(juzhen a, juzhen b, juzhen &c) { int s=0; for (int i = 1; i <= a.m1(); i++)//A矩阵的M for (int j = 1; j <= b.n1(); j++)//B矩阵的S { for (k0 = 1; k0 <= a.n1(); k0++)//a.n1也就是b.m1(a的n,b的n)[行向量*列向量

随机推荐