相对于面向过程的c语言:根据程序的执行过程,来设计软件的所有细节。c++是一门面向对象的语言。很多人在面试的时候是不是都被问过这样一个问题:c/c++有什么区别和联系?这里我就不多做介绍了,网上有很多这个问题的回复。
今天讲述的主角可不是这个,而且相对于c++有独特特性的类中的构造函数。
构造函数的作用
构造函数大概的含义就是类通过一个或者几个特殊的成员函数来控制其对象的初始化过程。
个人对构造函数的理解就是:它是类的入口,主要任务就是初始化对象的数据成员,无论何时只要类的对象被创建/在创建一个新对象的时候,就会自动调用构造函数,来进行初始化工作(对这个类对象的内部成员数据进行初始化工作)。
构造函数的语法
构造函数的名字必须跟定义的类名相同,与其他函数不一样,构造函数一定没有返回值,而且必须是公有成员,因为私有成员是不允许外部访问的,且函数不能声明为const类型。
大概语法如下:
class test() //test为类名
{
public:
test() { std::cout<<"this is a test.."<<std::endl; }
};
int main()
{
test object; //类实例
return 0;
}
在main函数执行之前,object被定义时就会调用类名构造函数,输出"this is a test…"。
这里只是示范了一个最简单的构造函数的形式,其实构造函数是个比较复杂的部分,它有非常多神奇的特性。
构造函数的种类
(1)默认构造函数
(2)自定义带参数的构造函数
(3)拷贝构造函数
(4)赋值构造函数
1 默认构造函数
指的是没有参数的默认构造函数
一般当程序中创建一个类后,但你没有写任何构造函数(即没有显示的定义构造函数时),则系统会自动生成默认的无参构造函数,这种编译器创建的构造函数又被称为合成的默认构造函数。函数里面为空,什么都不做。
当调用合成的默认构造函数时,它的初始化规则大概是下面这样的:
(1)如果存在类内的初始值,用它来初始化成员。在C++11的新特性中,C++11支持为类内的数据成员提供一个初始值,创建对象时,类内初始值将用于初始化数据成员。如果在构造函数中又显式地初始化了数据成员,则使用显式初始化的值。
(2)否则,默认初始化该成员(实际上,一般不做任何初始化)。默认初始化意味着和C语言一样的初始化方式,当类对象为全局变量时,在系统加载时初始化为0,而作为局部变量时,由于数据在栈上分配,成员变量值不确定。这也是很多情况下,当忘记给某个值赋值时,它调用显示出来是一个不认识的无序值的原因。
示例代码1.1:
#include <iostream>
#include <Windows.h>
#include <string>
using std::string;
// 定义一个“人类”
class Human {
public: //公有的,对外的
string getName();
int getAge();
int getSalary();
private:
string name;
int age = 18; //使用了类内初始化
int salary;
};
string Human::getName() {
return name;
}
int Human::getAge() {
return age;
}
int Human::getSalary() {
return salary;
}
int main(void) {
Human human; // 使用合成的默认初始化构造函数
std::cout << "年龄: " << human.getAge() <<std::endl; //使用了类内初始值
std::cout << "薪资:" << human.getSalary() <<std::endl; //没有类内初始值
system("pause");
return 0;
}
运行出来的结果为:
第二个值很大的原因是因为没有被主管自定义初始化,而是系统进行默认初始化。
注意:
【仅当数据成员全部使用了“类内初始值”,才宜使用“合成的默认构造函数”】
(1)一般只要我们自己手动定义了任何一种构造函数,系统就不会再自动生成一个"合成的默认构造函数"。
(2)一般情况下都应该自己去定义一个构造函数,哪怕是一个无参的构造函数,也需要自己去定义显示出来,而不是使用合成的默认构造函数。
(3)仅当数据成员全部使用了"类内初始值",才适合使用“合成的默认构造函数”。其他情况下不推荐使用默认构造函数。
使用自定义默认构造函数时
示例代码1.2:
#include <iostream>
#include <Windows.h>
#include <string>
using std::string;
// 定义一个“人类”
class Human {
public: //公有的,对外的
Human(); //手动定义的“默认构造函数”
string getName();
int getAge();
int getSalary();
private:
string name = "zhang";
int age = 28;
int salary;
};
//默认构造函数实现
Human::Human() {
name = "无名氏";
age = 18;
salary = 30000;
}
string Human::getName() {
return name;
}
int Human::getAge() {
return age;
}
int Human::getSalary() {
return salary;
}
int main(void) {
Human human; // 使用自定义的默认构造函数
cout << "姓名:" << human.getName() << endl;
cout << "年龄: " << human.getAge() << endl;
cout << "薪资:" << human.getSalary() << endl;
system("pause");
return 0;
}
归纳:
如果某数据成员使用类内初始值,同时又在构造函数中进行了初始化,那么以构造函数中的初始化为准。简而言之就是自定义默认构造函数中的初始化,会覆盖对应的类内初始值。
2 自定义带参数的构造函数
也称为一般构造函数或者重载构造函数。一般构造函数可以有各种参数形式,一个类可以有多个一般构造函数,前提是参数的个数或者类型不同(基于c++的重载函数原理)。如果定义多个,则在创建对象时会根据传入的参数不同调用不同的构造函数。
示例代码2.1:
#include <iostream>
#include <Windows.h>
#include <string>
using std::string;
// 定义一个“人类”
class Human {
public:
Human(); //自定义默认构造函数
Human(int nAge, int nSalary); //自定义带参构造函数
void eat();
void sleep();
void play();
void work();
string getName();
int getAge();
int getSalary();
private:
string name = "zhang";
int age = 28;
int salary;
};
Human::Human() {
name = "无名氏";
age = 18;
salary = 30000;
}
Human::Human(int nAge, int nSalary) {
std::cout << "调用自定义的构造函数" << std::endl;
this->age = nAge; //this是一个特殊的指针,指向这个对象本身
this->salary = nSalary;
name = "无名";
}
string Human::getName() {
return name;
}
int Human::getAge() {
return age;
}
int Human::getSalary() {
return salary;
}
int main(void) {
//Human human(); //调用自定义默认构造函数
Human human(25, 35000); // 使用自定义的默认构造函数
std::cout << "姓名:" << human.getName() << std::endl;
std::cout << "年龄: " << human.getAge() << std::endl;
std::cout << "薪资:" << human.getSalary() << std::endl;
system("pause");
return 0;
}
3 拷贝构造函数
也称为复制构造函数。
说明:
(1)拷贝构造(复制构造)函数参数为类对象本身的引用,用于根据一个已存在的对象复制出一个新的该类的对象,一般在函数中会将已存在对象的数据成员的值复制一份到新创建的对象中。
(2)若没有显式的写拷贝构造(复制构造)函数,则系统会默认创建一个复制构造函数(下面说的合成的拷贝构造函数),但当类中有指针成员时,由系统默认创建该复制构造函数会存在风险,原因就是会进行 “浅拷贝” 或“深拷贝”,下面会进行详细说明。
拷贝构造函数一般形式如下:
class Test
{
public:
Test(const Test &ob){
// 将对象ob中的数据成员值复制过来
x = ob.x;
y = ob.y;
}
private:
int x;
int y;
};
可以看出,构造过程就是将另一个同类对象的成员变量一一赋值,const修饰是因为限定传入对象的只读属性。
有没有发现一个问题,就是上面的私有变量在函数实现里面被直接访问了。按照一般逻辑,不是应该公有函数(get…()开头的公有函数)才能访问到吗?其实这种逻辑是错误的,之所以调用get来获得私有变量,是因为这样写更容易通俗易懂,可读性更好,封装性更好。就算我们随便定义一个函数,都可以访问同类的私有变量。
原因在于:类的封装性是针对类而不是针对类对象的。通俗来说,我们定义类中成员访问权限的初衷是为了保护私有成员不被外部其他对象访问到,一般情况下私有成员被外部访问的方式都是通过公共的函数接口(public),而在类的内部,任何成员函数都能访问私有成员,这种保护是针对不同的类之间的,所以我们是在定义类的时候来指定访问权限,而不是在定义对象的时候再指定访问权限。
再者,相同类对象,对于所有的私有变量,彼此知根知底,也就没有什么保护的必要。既然是这样,类内的构造函数以及其它函数都是类的成员函数,自然可以访问所有数据。
注:补充
重载赋值运算符的实现,即“=”
示例代码3.1:
class Test
{
public:
Test& operator=(const Test &ob){
// 首先检测等号右边的是否就是左边的对象本,若是本对象本身,则直接返回
if ( this == &ob)
{
return *this;
}
//复制等号右边的成员到左边的对象中
this->x = ob.x;
this->y = ob.y;
// 把等号左边的对象再次传出
// 目的是为了支持连等 eg: a=b=c 系统首先运行 b=c
// 然后运行 a= ( b=c的返回值,这里应该是复制c值后的b对象)
return *this;
}
private:
int x;
int y;
};
有没有注意到:
3.1代码中的等号运算符重载类似复制构造函数,将=右边的本类对象的值复制给等号左边的对象,但它不属于构造函数,等号左右两边的对象必须已经被创建。
若没有显示的写=运算符重载,则系统也会创建一个默认的=运算符重载,只做一些基本的拷贝工作。
调用实现说明:
void main()
{
// 调用了无参构造函数,数据成员初值被赋为0.0
Test c1;
// 调用一般构造函数,数据成员初值被赋为指定值
Test c2(20,5000);
// 也可以使用下面的形式
Test c2 = Test(20,5000);
// 把c2的数据成员的值赋值给c1
// 由于c1已经事先被创建,故此处不会调用任何构造函数
// 只会调用 = 号运算符重载函数
c1 = c2;
// 自定义有参构造函数
// 系统首先调用自定义有参构造函数,将30创建为一个本类的临时对象,然后调用等号运算符重载,将该临时对象赋值给c1
c2 = 30;
// 调用拷贝构造函数( 有下面两种调用方式)
Test c4(c2);
Test c3 = c2; // 注意和 = 运算符重载区分,这里等号左边的对象不是事先已经创建,故需要调用拷贝构造函数,参数为c2
}
拷贝构造总体分为两种
(1)合成的拷贝构造函数(系统默认生成的)
如果没有自己定义拷贝构造函数,那当进行上面类似Test c4(c2)或Test c3 = c2的时候就会调用合成的拷贝构造函数。
(2)手动定义的拷贝构造函数
示例3.2:
#include <iostream>
#include <Windows.h>
#include <string>
using std::string;
class Human {
public:
Human();
Human(int age, int salary); //有参构造函数
//如果不定义拷贝构造函数,编译器会生成“合成的拷贝构造函数”
Human(const Human&); //手动定义拷贝构造
string getName();
int getAge();
int getSalary();
void setAddr(const char *newAddr);
const char* getAddr();
private:
string name = "zhang";
int age = 28;
int salary;
char *addr;
};
Human::Human() {
name = "无名氏";
age = 18;
salary = 30000;
}
Human::Human(int age, int salary) {
std::cout << "调用自定义的构造函数" << std::endl;
this->age = age; //this是一个特殊的指针,指向这个对象本身
this->salary = salary;
name = "无名";
}
Human::Human(const Human& man) {
std::cout << "调用自定义的拷贝构造函数" << std::endl;
name = man.name;
age = man.age;
salary = man.salary;
}
string Human::getName() {
return name;
}
int Human::getAge() {
return age;
}
int Human::getSalary() {
return salary;
}
void Human::setAddr(const char *newAddr) {
if (!newAddr) {
return;
}
strcpy_s(addr, 64, newAddr);
}
const char* Human::getAddr() {
return addr;
}
int main(void) {
Human h1(20, 30000); // 调用自定义的默认构造函数
Human h2(h1); // 调用自定义的拷贝构造函数 //如果没有自定义,那会调用默认的合成拷贝构造函数
Human h3 = h1; // 也会调用自定义的拷贝构造函数
std::cout << "姓名:" << h2.getName() << std::endl;
std::cout << "年龄: " << h2.getAge() << std::endl;
system("pause");
return 0;
}
看到这,你会不会这样想,既然我不自定义也会触发系统的默认合成拷贝构造函数将对应的成员一一赋值,那我干嘛非要自己定义一个拷贝构造函数。这不是多此一举吗?
下面我们就来看看合成的拷贝构造函数有哪些坑,看完后你应该就知道了为什么要自定义一个拷贝构造函数了。
在代码3.2的main函数system(“pause”)上一行代码中添加如下代码,并且将手动定义的拷贝构造函数注释掉,这样才会调用系统合成拷贝构造函数:
std::cout << "h1 addr:" << h1.getAddr() << std::endl;
std::cout << "h2 addr:" << h2.getAddr() << std::endl;
h1.setAddr("合肥");
std::cout << "h1 addr:" << h1.getAddr() << std::endl;
std::cout << "h2 addr:" << h2.getAddr() << std::endl;
结果运行出来的
这里有一个很重要的问题被触发了,那就是我只是改动了h1这个对象的地址值,但运行结果就是不仅改变了h1的地址,h2的地址也被改动了。
造成这样的原因是:合成的拷贝构造函数,它使用的是“浅拷贝”
分析原因:
如果没有自定义拷贝(复制)构造函数,则系统会创建默认的复制构造函数,但系统创建的默认复制构造函数只会执行“浅拷贝”,即将被拷贝对象的数据成员的值一一赋值给新创建的对象,若该类的数据成员中有指针成员,则会使得新的对象的指针所指向的地址与被拷贝对象的指针所指向的地址相同(两个指向同一地址),并且当我们进行delete该指针时则会导致两次重复delete而出错,因为同一个地址会被delete两次。
视图说明:
解决方案:
在自定义的拷贝构造函数中,使用‘深拷贝。
将3.2代码被注释的自定义拷贝构造函数放开,并将函数实现里面添加上两行深拷贝的代码。如下:
Human::Human(const Human &man) {
cout << "调用自定义的拷贝构造函数" << endl;
age = man.age; //this是一个特殊的指针,指向这个对象本身
salary = man.salary;
name = man.name;
// 深度拷贝
addr = new char[64];
strcpy_s(addr, 64, man.addr);
}
这时候我们再次运行程序时:
这样就不会出现改动一个,而地址全部改变的情况了。这样也就将上面刚开始拷贝构造函数的深浅拷贝的坑填上了。
结论:
所以,在使用编译器默认的合成构造函数时,我们要非常小心这一类的陷阱,即使是目前没有指针成员函数,也要自己写拷贝赋值构造函数,这样有利于代码的扩展和维护。
但是,话说回来,如果我每次实现一个很简单的需求,都要定义复制拷贝构造函数,一个一个成员去赋值,这样也是很烦人的,在新标准下,C++提供了一种方法来"解决"这个问题。
新标准给出了一个新的用法delete。也就是阻止拷贝
用法大概如下:
//如果不定义拷贝构造函数,编译器会生成“合成的拷贝构造函数”
Human(const Human&) = delete; //手动定义拷贝构造
//Human &operator(const Human&) = delete;
//事实上,部分编译器默认已经禁止了合成的拷贝赋值构造函数。
当在拷贝构造函数后面加上了delete后。再使用默认的拷贝构造函数或者自定义的拷贝构造函数时,编译器会给我们报错。
调用拷贝构造函数的时机
(1)调用函数时,实参是对象,形参不是引用类型 如果函数的形参是引用类型,就不会调用拷贝构造函数
(2)函数的返回类型是类,而且不是引用类型
(3)对象数组的初始化列表中,使用对象。
示例代码讲解说明:
#include <iostream>
#include <Windows.h>
#include <string>
using std::string;
class Human
{
public:
Human();
Human(int age, int salary);
Human(const Human&); //不定义拷贝构造函数,编译器会生成“合成的拷贝构造函数”
int getSalary();
private:
string name = "zhang";
int age = 28;
int salary;
char *addr;
};
Human::Human() {
name = "无名氏";
age = 18;
salary = 30000;
}
Human::Human(int age, int salary) {
std::cout << "调用自定义的构造函数" << std::endl;
this->age = age; //this是一个特殊的指针,指向这个对象本身
this->salary = salary;
name = "无名";
addr = new char[64];
strcpy_s(addr, 64, "China");
}
Human::Human(const Human &man) {
std::cout << "调用自定义的拷贝构造函数" << "参数:" << &man << " 本对象:" << this << std::endl;
age = man.age; //this是一个特殊的指针,指向这个对象本身
salary = man.salary;
name = man.name;
// 深度拷贝
addr = new char[64];
strcpy_s(addr, 64, man.addr);
}
int Human::getSalary() {
return salary;
}
void test(Human man) {
std::cout << man.getSalary() << std::endl;
}
void test2(Human &man) { //不会调用拷贝构造函数,此时没有构造新的对象
std::cout << man.getSalary() << std::endl;
}
Human test3(Human &man) {
return man;
}
Human& test4(Human &man) {
return man;
}
int main(void) {
Human h1(25, 35000); // 调用自定义构造函数
std::cout<<"1"<<std::endl;
Human h2(h1); // 调用拷贝构造函数
std::cout<<"2"<<std::endl;
Human h3 = h1; // 调用拷贝构造函数
std::cout<<"3"<<std::endl;
test(h1); // 调用拷贝构造函数
std::cout<<"4"<<std::endl;
test2(h1); // 不会调用拷贝构造函数
std::cout<<"5"<<std::endl;
test3(h1); // 创建一个临时对象,接收test3函数的返回值,调用1次拷贝构造函数
std::cout<<"6"<<std::endl;
Human h4 = test3(h1); // 仅调用1次拷贝构造函数,返回的值直接作为h4的拷贝构造函数的参数
std::cout<<"7"<<std::endl;
test4(h1); // 因为返回的是引用类型,所以不会创建临时对象,不会调用拷贝构造函数
std::cout<<"8"<<std::endl;
Human men[] = { h1, h2, h3 }; //调用3次拷贝构造函数
system("pause");
return 0;
}
为了更方便的观察打印结果,我在每一次调用后都加了打印信息,便于观察:
花了一晚上的时间暂时整理好了默认构造,自定义构造和拷贝构造的有关实践笔记。由于篇幅太长,剩下的赋值构造函数和析构函数将放在下一篇文章中介绍。