目录
构造函数
1.构造函数:
2.构造函数的特点:
默认构造函数 -- 没有参数的构造函数
1. 合成(自动)的默认构造函数(一般不常用)
1) 介绍,以及为什么不使用
2)可以使用合成默认构造函数的情况
2. 自定义的默认构造函数
1)介绍
有参构造函数 -- 带参数的构造函数
1. 自定义有参构造函数
1) 简述
2) 使用有参构造函数实例化对象
3)有参构造函数定义时需要注意:
1. 使用的参数名字和类内的属性名字不同
2. 使用的构造函数参数的名字和类内的属性名字一样
3. 解决办法: 使用this指针 :指向当前对象(可以理解为调用函数的这个对象)
拷贝构造函数
1. 合成的拷贝构造函数(有很大风险) -- 浅拷贝
1)简述 -- 浅拷贝
2)合成拷贝构造函数具有很大的风险(浅拷贝的风险)
2. 自定义的拷贝构造函数 -- 深拷贝
1)简述 -- 深拷贝
2)使用深拷贝来解决浅拷贝的问题
3)浅拷贝和深拷贝
4)拷贝构造函数的调用时机
返回过程:
5) 拷贝构造函数注意事项
赋值构造函数
合成的赋值构造函数
1)简述
2) 代码
3)合成赋值构造函数也存在问题 -- 也是浅拷贝
自定义的赋值构造函数
1)含义
2)代码
3)赋值构造函数的写法
总结:
构造函数是c++类中的概念,用来给对象的属性进行初始化和一些其它的操作(相当于创造一个东西时,赋予它一些特性)。
其实从字面意思来理解,就是创造一个东西的函数,对于类来说这个东西就是对象。
所以,c++语法规定,在我们实例化一个类对象的时候,都会默认调用构造函数。
1) 创建对象时自己调用
2) 没有返回值,函数名和类名一样
3) 可以利用函数的重载,写多个构造函数。
构造函数必须设置为公有,因为我们实例化对象在类外,所以构造函数必须能够被外界使用。
我们上面说了,c++的语法规则:实例化一个对象就会自动调用构造函数。
但是如果我们没有在类中写构造函数呢? 那么编译器会默认给我们生成一个合成的默认构造函数。这个构造函数其实内部什么也没有,就是一个空函数。 那没什么要有它呢?-- 也许为了语法统一。
但是,使用合成的默认构造函数的风险很高。 看代码:
class Human {
public:
int getAge();
void visit();
private:
int age;
};
int Human::getAge() {
return this->age;
}
void Human::visit() {
cout << getAge() << endl;
}
int main(void) {
Human man; // 创建man对象
man.visit(); // 使用visit函数来打印age的值
system("pause");
return 0;
}
上面代码,我们创建了一个对象,并且我们没有在类中实现构造函数,这是编译器会自己创建一个构造函数(空实现)。
我们通过visit()打印通过对外接口getAge()获得的age的值。-- 看上面的结果,是一个很大的负值。这中情况很常见 -- 没有赋值直接使用。
为什么会是上面的结果呢?
因为,对象man中的属性age,没有赋值,在实例化的时候,调用的也是编译器自己创建的默认构造函数,也是空实现,不会对age进行赋值,所以age在使用的时候是没有赋值的。所以会出现问题。
这就是为什么不使用它的原因了。
有一种情况,我们是可以使用默认构造函数的。
class Human {
public:
int getAge();
void visit();
private:
int age = 18; // 仅c++11之后可以
};
比如代码中,我们在类内定义的age变量,定义的时候已经初始化了。这时,即使再输出age也不会出现上面的情况(构造函数不赋值的情况下)。
所以,当我们类内的数据在定义的时候全都已经初始化了(必须全部初始化,因为有不初始化的就存在风险),就可以使用合成默认构造函数了。
注意: 只有c++11之后才支持在类内定义数据时,进行初始化。
上面说到,我们一般都不会使用合成默认构造函数,我们一般都会自己定义一个构造函数。
在自定义的构造函数中我们就可以对相应的数据进行初始化了。
class Human {
public:
int getAge();
void visit();
Human();
private:
int age;
};
int Human::getAge() {
return this->age;
}
Human::Human() {
age = 18;
}
void Human::visit() {
cout << getAge() << endl;
}
int main(void) {
Human man; // 创建man对象
man.visit(); // 使用visit函数来打印age的值
system("pause");
return 0;
}
上面代码,就定义了一个默认构造函数,Human(),我们在实现构造函数中,将age属性进行了初始化。
在自定义的构造函数中我们可以根据自己的需要进行设置,将相应的属性进行初始化。
注意: 当我们自己定义了构造函数的时候,编译器就不会给我们提供了。
前面提到构造函数可以有多个,带参数的构造函数是我们自己写的。 前面写的默认构造函数没有参数的。
有参构造函数:就是带参数的构造函数,所以它的书写规则和无参构造很类似,就是带了个参数。
class Human {
public:
Human(int age, string name);
int getAge();
string getName();
void visit();
private:
int age;
string name;
};
int Human::getAge() {
return this->age;
}
string Human::getName() {
return name;
}
void Human::visit(){
cout << getAge() << endl;
cout << getName() << endl;
}
Human::Human(int age, string name) {
this->age = age; // 如果名字相同使用this指针,或者可以将参数的名字设置成不一样的
this->name = name;
}
int main(void) {
Human man1(18, "男神");
man1.visit();
system("pause");
return 0;
}
代码中:定义了一个有参构造函数:Human(int age,string name) ;
代码中有参构造有两个参数,用来接收用户用来初始化属性的值。
所以在使用有参构造函数创建对象的时候,需要传入参数。Human man1(18,"男神"); 就是调用有参构造函数进行创建对象。
class Human {
public:
// 有参构造函数
Human(int age1, string name1);
private:
int age;
string name;
};
Human::Human(int age1, string name1) {
age = age1;
name = name1;
}
当有参构造函数的参数名字和类内属性名,名字不一样的时候,可以直接进行赋值。
class Human {
public:
// 有参构造函数
Human(int age, string name);
private:
int age;
string name;
};
Human::Human(int age, string name) {
age = age; // 此时这两个age都表示的是参数,所以不正确。name类似
name = name;
}
当参数的名字和属性名字相同的时候,就不能直接赋值了。
age = age; // 此时这两个age都表示的是形参里面的age,并不是类内属性的age,所以不对。
Human::Human(int age, string name) {
this->age = age;
this->name = name;
}
使用this之后,就可以了。 因为this指向当前对象,所以this->age就是指当前对象的age属性。
拷贝构造函数:在一个对象初始化的时候,给它赋值另外一个对象,这是就会默认调用拷贝构造函数。
Human man2 = man1;
合成的拷贝构造函数就是:当自己没有实现拷贝构造函数的时候,编译器会自动生成一个合成拷贝构造函数。
拷贝构造函数会将man1中属性的值拷贝到man2的属性中。
浅拷贝:就是只将属性的值,拷贝到另外一个对象。
这样看,浅拷贝好像没有什么问题。如果使用普通的变量,没有什么问题,但是如果属性中指针,那么就会出现问题。
举个例子:
int age;
string name;
char *a;
man1 : age=12,name="帅哥",*a = 'a'
Human man2 = man1;
将man1中的值拷贝到man2中, 我们没有定义拷贝构造函数,所以编译器自动生成,进行浅拷贝: 将man1 中 age 的值复制给 man2中 age,name也同理,
重点来看指针的拷贝:浅拷贝只是将指针变量a中存放的地址拷贝到man2的a中,也就是说进行浅拷贝之后,man1中的a和man2中的a指向同一片内存,同一块数据。
可能会疑问问什么不能指向同一个呢? -- 很简单,man1 和 man2两个对象毫不相关,如果这个指针用来表示的是资产,那你说,两个人的资产怎么能放到一起呢。
所以这样用一定会出问题,会有很大的风险。
前面说到,系统自动提供的拷贝构造函数只能进行浅拷贝,会造成很大的风险。
那怎么办呢?-- 那就自己实现一个呗,然后使用深拷贝来解决这个问题
浅拷贝的问题是: 对指针进行拷贝时,不会开辟新空间,直接将新对象的指针指向别的对象的内存,这就会造成很大的风险。
问:那深拷贝如何解决这个问题呢?
我们可以在自己实现的拷贝构造函数中,给新对象的指针开辟一片空间,来存放其他对象指针中的值 -- 这样就不会共用同一片空间了。
class Human {
public:
Human();
Human(const Human&); // 函数声明可以不写参数名称
private:
int age;
string name;
char* f1;
};
Human::Human() {
age = 20;
name = "帅哥";
f1 = new char[10];
strcpy_s(f1,10,"好好学习"); // 使用字符串拷贝给字符串进行赋值
}
Human::Human(const Human& man) {
age = man.age;
name = man.name;
f1 = new char[10];
strcpy_s(f1, 10, man.f1); // 将man中f1指向内存的值拷贝到此对象的f1中
}
int main(void) {
Human man1;
Human man2 = man1; // 在对象初始化时赋值给另外一个对象,则会默认调用拷贝构造函数
system("pause");
return 0;
}
Human(const Human&); 就是我们自定义的拷贝构造函数声明。
在函数实现的过程中, 我们又为新对象的f1指针创建了一片内存,用来存放其它对象的值。
浅拷贝:系统自己生成的拷贝构造函数,只是将变量中的值浅浅的复制过去,不管你是不是有风险, 比如:对于指针变量,只是将指针变量存放的地址复制过去,使它指向了同一片空间。
深拷贝: 为了解决浅拷贝带来的风险,我们需要自己实现拷贝构造函数,为新对象的指针开辟空间,将别的对象指向的内容复制过去,而不是简单的将指针中存储的地址复制过去。
1. 当函数参数不是引用(值传递),参数类型是我们定义的类的时候
void test(Human man) { // 传参数的过程就是 Human man = man1 这不就是拷贝构造
// 测试语句
}
int main(void) {
Human man1;
test(man1);
system("pause");
return 0;
}
2. 返回值为我们定义的类
Human test(Human& man) {
// 测试语句
return man;
}
int main(void) {
Human man1;
test(man1);
system("pause");
return 0;
}
其实就是创建一片临时空间来存放man的值,然后返回给主调函数。 这个过程其实和Human man1 = man;是类似的,只是此处我们不知道变量名称。
3. 初始化对象时,直接使用另外一个对象初始化,使用=或者()都可以。都会自动调用拷贝构造函数。
int main(void) {
Human man1;
Human man2 = man1; // 在对象初始化时赋值给另外一个对象,则会默认调用拷贝构造函数
Human man3(man1);
system("pause");
return 0;
}
4. 使用对象数组初始化的时候
int main(void) {
Human man1;
Human man2 = man1;
Human man3(man1);
Human man4[3] = { man1,man2,man3};
system("pause");
return 0;
}
其实就是定义了一个数组,数组中的每个元素都是Human类型,所以其实就相当于:
Human man4[0] = man1; Human man4[1] = man2; Human man4[2] = man3; 自动调用拷贝构造函数。
1. 拷贝构造函数的参数类型必须是: const Human& man 这种
2. 拷贝构造函数的参数只能有一个,因为初始化的时候,只能一个对象作为右值。
3. 由于2.,所有拷贝构造函数只能有一个,因为它无法进行函数重载,因为它只能有一个参数,并且参数的类型和数量都是固定的。 (有参构造可以重载,因为参数个数个类型不受限制)
赋值构造函数和其他的构造函数不同(具体看实现)。 合成赋值构造函数也是系统自己提供
int main(void) {
Human man1,man2;
man2 = man1; // 在不初始化时,进行赋值,调用赋值构造函数
system("pause");
return 0;
}
当我们使用=将一个对象赋值给另外一个对象的时候,系统会自动调用赋值构造函数。(注意:不是初始化的时候,初始化时调用的是拷贝构造函数)
还是浅拷贝,当属性中有指针时,虽然两个对象的指针都指向各自的内存,但是合成赋值构造函数只是进行简单的赋值,对指针进行赋值时,只是将指针变量的值赋值给新对象的指针,这样又会导致两个对象的指针直系那个同一片内存。(浅拷贝)
和上面一样,既然浅拷贝有问题,那么我们就自定义一个赋值构造函数来实现深拷贝。
class Human {
public:
Human();
Human& operator=(const Human& man); // 赋值构造函数 -- =运算符重载
private:
int age;
string name;
char* f1;
};
Human::Human() {
age = 20;
name = "帅哥";
f1 = new char[10];
strcpy_s(f1,10,"好好学习"); // 使用字符串拷贝给字符串进行赋值
}
Human& Human::operator=(const Human& man) {
age = man.age;
name = man.name;
// 深拷贝
strcpy_s(f1, 10, man.f1);
return *this; // 返回此对象
}
int main(void) {
Human man1,man2;
man2 = man1; // 在不初始化时,进行赋值,调用赋值构造函数
system("pause");
return 0;
}
问:为什么此处的深拷贝不需要开辟新空间?
此处的深拷贝,已经不需要开辟空间了,因为我们在实例化对象的时候 ,已经在默认构造函数中,对每个对象的f1指针都开辟了单独的空间。所以,不需要再开辟空间了。
我们只需要将man1中的f1内存中的值拷贝到man2的f1指向的内存即可。(而不是,将f1中的地址赋值过去)。
1. 首先赋值构造函数就是一个=的重载函数
2. 参数传入的是作为=右值的对象,而调用函数的是=左值的对象。(this指针指向当前对象 )
3. 返回Human&, 是用来返回调用函数的对象的。this指向调用函数的当前对象,*this就是此对象。
4. 3.的原因: 为了能够实现连续赋值的情况 -- man = man1 = man2;
man1 = ma2; 调用赋值构造函数,返回man1, 再进行 man = man1。
综合上面的看,构造函数其实就是,当我们对对象进行相应的操作时,系统会自动去调用的函数。为了构造当前对象,去调用函数。