小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。
一、浅拷贝和深拷贝
浅拷贝:
- 同一块空间释放两次,称之为浅拷贝,会造成程序异常结束的错误
- 主要是在类中成员变量有指针的时候可能会出现的问题
- 两个指针保存的是同一个堆区的地址,结果使用完毕后释放了两次
深拷贝:
- 两个指针变量的空间是不一样的,如果都释放空间是不会有影响的
//浅拷贝:
void test2()
{
char *s1 = new char[32];
strcpy(s1, "nihao beijing");
cout << s1 << endl;
char *s2 = s1;
cout << s2 << endl;
delete []s1;
delete []s2;
}
//深拷贝:
//两个指针变量的空间是不一样的,如果都释放空间是不会有影响的
void test3()
{
char *s1 = new char[32];
strcpy(s1, "nihao beijing");
cout << s1 << endl;
char *s2 = new char[strlen(s1) + 1];
strcpy(s2, s1);
cout << s2 << endl;
delete []s1;
delete []s2;
}
二、三个非常重要的自动调用的成员函数
自动调用:意味着这三个函数会在特定的场合自动调用,并且手动调用还不行,既然C++类中有自动调用的成员函数,所以看C++的代码就不能只看main函数。
三个函数都是公有的
三个重要的成员函数:
- 构造函数
- 析构函数
- 拷贝构造函数
-
构造函数:
用于一个类实例化对象的同时赋值(初始化) -
析构函数:
主要用于释放成员变量的空间 注意:析构函数不是每次都会调用,可能有一个指针变量,并且要在堆区开辟空间,就会调用析构函数来释放空间 -
拷贝构造函数:
主要就是当实例化一个对象的时候用另一个对象给其赋值的时候自动调用Cube c2 = c1;//这种情况会调用拷贝构造函数Cube c3; c3 = c1;//这种情况不会调用拷贝构造函数
默认如果自己不写这三个函数,会自动调用系统默认给的这三个函数,意味着系统会提供者三个函数,但是一旦自己重新写了,那么系统的会失效系统默认的构造函数和析构函数都为空,而拷贝构造函数只是值传递。
注意:如果成员变量没有指针变量,不需要自己写析构函数了,默认值传递就可以了,为啥会让你调用,就是怕有堆区的空间没有释放
#include <iostream>
using namespace std;
class Person{
public:
//定义一个构造函数
Person()
{
cout << "无参构造函数" << endl;
}
Person(int a, string b, int c)
{
cout << "有参构造函数" << endl;
id = a;
name = b;
score = c;
}
//析构函数
~Person()
{
cout << "析构函数" << endl;
}
//拷贝构造函数
Person(const Person &obj)
{
cout << "拷贝构造函数" << endl;
id = obj.id;
name = obj.name;
score = obj.score;
}
void printMsg()
{
cout << id << ", " << name << ", " << score << endl;
}
private:
int id;
string name;
int score;
};
void test1()
{
Person p1; //无参构造函数
Person p2(1001, "张三", 90); //有参构造函数
p2.printMsg();
Person p3 = p2; //拷贝构造函数
p3.printMsg();
cout << "****************" << endl;
Person p4; //无参构造函数
p4 = p2; //这样赋值不会调用拷贝构造函数
p4.printMsg();
} //函数执行结束,对象释放空间,自动调用析构函数
int main()
{
test1();
return 0;
}
2.1构造函数
2.1.1 构造函数的基本使用
定义一个构造函数
- 构造函数的名字就是类名
- 构造函数没有返回值,连void也不能写
- 构造函数是可以重载的
- 如果要写有参构造函数,就必须要有无参构造函数,
- 因为只要自己写了构造函数,系统默认的将不会再调用(写有参构造必须有无参构造,因为我们习惯实例化对象的时候不给赋初值,如果不写有参构造的话,两个都不用写,因为默认的就是有参构造)
有了构造函数是为了初始化方便
调用时机
-
类实例化对象的时候自动调用(类的变量就叫做对象,实例化是指实例一个对象的过程)
-
栈区:
类名 对象名(构造函数的实参表);
类名 对象名 = 类名 (构造函数的实参表);
-
堆区:
类名 *指针;
指针 = new 类名(构造函数的实参表);
定义构造函数的要求
- 1.构造函数名与类同名
- 2.访问权限为public
- 3.没有返回值
构造函数用冒号的形式引出 初始化列表
格式:构造函数名(形参表):成员变量1(初始值1),成员变量2(初始值2)
例:
//初始化列表可以用实参传递给形参的值
//初始化列表也可以做申请空间的操作
Student::Student(string n,int a):name(n),age(new int(a)){
//其他的逻辑
}
//也可以直接给定值 如果直接给定 那实例化的对象的名字就全是 小明
Student::Student(string n,int a):name("小明"),age(new int(a)){
//其他的逻辑
}
几个必须使用初始化列表的场景
1.当类中包含只读成员 (const修饰的成员时)
private:
const string name;
2.当类中包含引用类型的时候
private:
string &name;
3.当成员函数的形参表中的变量名与类的成员变量名冲突时,如果不使用this指针,必须要用初始化表的形式给成员变量初始化
Student::Student(string name,int a):name(name),age(new int(a)){
cout<<"有参构造函数"<<endl;
//name = name;//错误的,不会报错,只不过没有把值赋给成员变量
}
4.类中包含其他类的对象时,必须使用初始化表的方式完成对类中其他类对象的成员变量的初始化
#include <iostream>
using namespace std;
class Person{
public:
Person()//系统默认
{
cout << "无参构造函数" << endl;//为了看清现象
}
#if 0
//常规构造函数赋值方法
Person(int id, string name, char sex, int score)
{
cout << "有参构造函数" << endl;
p_id = id;
p_name = name;
p_sex = sex;
p_score = score;
}
#endif
//使用初始化列表赋值
Person(int id, string name, char sex, int score):
p_id(id),p_name(name),p_sex(sex),p_score(score)
{
cout << "有参构造函数" << endl;
}
void printMsg()
{
cout << p_id << ", ";
cout << p_name << ", ";
cout << p_sex << ", ";
cout << p_score << endl;
}
private:
int p_id;
string p_name;
char p_sex;
int p_score;
};
void test1()
{
//此时调用的就是无参构造函数
Person p1;
Person p2(1001, "张三", 'M', 90);
p2.printMsg();
}
int main()
{
test1();
return 0;
}
执行结果:
2.1.2 构造函数的调用方式
- 方式1:隐式调用
- 方式2:显式调用==
Person p5 = Person; //此种方式不允许== - 方式3:动态开辟空间调用构造函数
- 方式4:定义匿名对象调用构造函数:
- 匿名对象的生存周期很短,定义的时候开辟空间,执行完毕后自动释放空间
- 匿名对象最主要的作用就是临时给其他对象赋值
- 除了以上四种还有一些冷门的
//构造函数的调用方式
void test2()
{
//方式1:隐式调用
Person p1;
Person p2(1001, "张三", 'M', 90);
cout << "************************" << endl;
//方式2:显式调用
Person p3 = Person();//记得加括号
Person p4 = Person(1002, "李四", 'W', 100);
//Person p5 = Person; //此种方式不允许,会报错
cout << "************************" << endl;
//方式3:动态开辟空间调用构造函数
Person *p6 = new Person;//d对比方式二,这样写是可以的,因为Person是个类型
Person *p7 = new Person();
Person *p8 = new Person(1003, "王五", 'M', 87);
cout << "************************" << endl;
//方式4:定义匿名对象调用构造函数
//匿名对象的生存周期很短,定义的时候开辟空间,执行完毕后自动释放空间
//匿名对象最主要的作用就是临时给其他对象赋值
//以后用到再说
Person();
Person(1004, "赵六", 'M', 87);
//Person(1004, "赵六", 'M', 87).printMsg();//这样可以打印
}
2.1.3 构造函数不会调用的情况
注意:
- 形参如果是一个对象,是不会调用构造函数的,加引用也不行
- 类实例化的对象接收函数返回值不会调用构造函数
- 不是说初始化一定会调用构造函数
//注意: 形参如果是一个对象,是不会调用构造函数的
Person myFun(Person p1)//Person &p1,加引用也不行
{
p1.printMsg();
cout << "--------------" << endl;
static Person p2(1004, "赵六", 'M', 87);
cout << "--------------" << endl;
return p2;
}
void test3()
{
Person p1(1001, "张三", 'M', 90);
cout << "*************" << endl;
//类实例化的对象接收函数返回值不会调用构造函数
Person p2 = myFun(p1);
cout << "*************" << endl;
p2.printMsg();
}
2.2 析构函数
析构函数:释放堆区的空间
- 释放p1只是释放了但是,堆区开辟的
p_name = new char[strlen(name) + 1];也需要释放,如果里面的成员是普通成员,是栈区开辟的,就不需要手动释放, - 释放p1的释放只是释放的是两个栈区的普通变量,堆区只是释放两个指针变量,空间没有释放
这样解释:
char *str = new char[32];
return str;
str是一个char *的str
char *str是栈区开辟的空间,这个空间所保存的值是堆区的地址new char[32]
程序结束后,栈区的空间释放了,但是堆区的空间没有释放,你只释放的是你这个指针变量的空间,但是你堆区开辟的空间没有释放,所以内存泄露;
那么 str 为什么能返回,因为返回值返回值,返回的是值,返回的是指针变量str的值,这个值就是堆区的地址;
所以最后栈区的空间释放了,但是堆区的地址返回了
定义一个析构函数
- 析构函数的名字也是类名
- 析构函数也没有返回值,连void也不写
- 析构函数没有参数,所以不能重载
- 析构函数定义的时候需要在函数名前加==~==
📣注意:
- 析构函数用于释放空间,释放的是成员变量的空间
- 当实例化的对象释放空间时,当前对象的析构函数才会调用
- 一个类中只能有一个析构函数
📝总结:
- 构造函数先调用的一般析构函数后调用,刚好与构造函数相反
- 堆区开辟空间如果不释放,程序结束也不会调用析构函数
- static修饰的对象的析构函数是在atexit函数执行之前调用的,==程序结束之前==
- 全局定义的对象的析构函数是在atexit函数执行完毕后调用的,==程序结束之后==
#include <iostream>
#include <cstdlib>
using namespace std;
class Person{
public:
Person(){cout << "无参构造函数" << endl;}
Person(int id, string name, char sex, int score):
p_id(id),p_name(name),p_sex(sex),p_score(score)
{
cout << "有参构造函数" << endl;
}
~Person()
{
cout << "析构函数" << endl;
cout << "this = " << this << endl;//任何一个类里面的成员函数,每一个对象再创建好之后,都会给每一个成员函数传一个自己的地址进去,这个指针是操作系统自动传的,不需要自己去传
}
void printMsg()
{
cout << p_id << ", ";
cout << p_name << ", ";
cout << p_sex << ", ";
cout << p_score << endl;
}
private:
int p_id;
string p_name;
char p_sex;
int p_score;
};
Person pp;
void test1()
{
Person p1;
cout << "&p1 = " << &p1 << endl;
Person p2(1001, "zhangsan", 'M', 90);
cout << "&p2 = " << &p2 << endl;
Person *p3 = new Person;
cout << "&p3 = " << p3 << endl;//注意这里打印的是p3
delete p3;
static Person p4;
cout << "&p4 = " << &p4 << endl;
cout << "--------------" << endl;
}
void myfun()
{
printf("hello world\n");
}
int main()
{
cout << "&pp = " << &pp << endl;
atexit(myfun);
cout << "*****************" << endl;
test1();
cout << "++++++++++++++++" << endl;
return 0;
}
执行结果:
无参构造函数 //全局变量pp
&pp = 0x40d040
*****************
无参构造函数 //无参构造函数p1
&p1 = 0x62fd80
有参构造函数 //有参构造函数p2
&p2 = 0x62fd50
无参构造函数 //无参构造函数,指针变量,堆区p3
&p3 = 0x62fd48
析构函数
this = 0x62fd48 //delete(p3);主动释放堆区p3
无参构造函数 //无参构造函数p4
&p4 = 0x40d080
--------------
析构函数 //p2
this = 0x62fd50
析构函数 //p1
this = 0x62fd80
++++++++++++++++
析构函数 //p4
this = 0x40d080
hello world //加上了atexit(myfun);不会增加 //堆区的就是程序已经结束了,它也没有主动释放,但是进程结束了,空间自动释放,因为堆区是用户空间的空间,进程结束用户空间自动释放
析构函数
this = 0x40d040 //pp
2.3 拷贝构造函数
==拷贝构造函数里面需要对指针变量重新开辟空间,防止浅拷贝==
-
==当实例化一个对象的同时用另一个对象对其初始化时调用拷贝构造函数==
-
拷贝构造函数就是特殊的构造函数
拷贝构造函数的其他调用方式
-
方式1:形参接收实参的值的时候会调用拷贝构造函数,==引用不会==
-
方式2:如果返回时是一个对象的时候会调用拷贝构造函数
-
以下形式也会调用拷贝构造函数,可以认为是一个匿名对象
myFun3();或myFun3().printMsg();
📣注意:
-
如果参数是定义的引用,不会调用拷贝构造函数,因为==没有开辟空间==
-
以下形式不会调用拷贝构造函数
Person p3;p3 = p1; -
返回值是引用不会调用拷贝构造函数
-
默认拷贝函数就是值传递
-
书写方式:
Person(const Person &obj),传引用,不用再开辟空间,传const保证我的值不会再改变
#include <iostream>
using namespace std;
class Person{
public:
Person(){cout << "无参构造函数" << endl;}
Person(int id, string name, char sex, int score):
p_id(id),p_name(name),p_sex(sex),p_score(score)
{
cout << "有参构造函数" << endl;
}
~Person()
{
cout << "析构函数" << endl;
}
//拷贝构造函数
// 当实例化一个对象的同时用另一个对象对其初始化时调用拷贝构造函数
// 拷贝构造函数就是特殊的构造函数
Person(const Person &obj)
{
cout << "拷贝构造函数" << endl;
p_id = obj.p_id; //值传递
p_name = obj.p_name; //值传递
p_sex = obj.p_sex; //值传递
p_score = obj.p_score; //值传递
}
void printMsg()
{
cout << p_id << ", ";
cout << p_name << ", ";
cout << p_sex << ", ";
cout << p_score << endl;
}
private:
int p_id;
string p_name;
char p_sex;
int p_score;
};
void test1()
{
Person p1(1001, "zhangsan", 'M', 90);
p1.printMsg();
Person p2 = p1;
p2.printMsg();
cout << "***************" << endl;
Person p3;
//注意:以下形式不会调用拷贝构造函数
p3 = p1;
}
//拷贝构造函数的其他调用方式
//方式1:形参接收实参的值的时候会调用拷贝构造函数
void myFun1(Person p) //Person p = p1
{
p.printMsg();
}
//注意:如果参数是定义的引用,不会调用拷贝构造函数
// 因为没有开辟空间
void myFun2(Person &p) //Person p = p1
{
p.printMsg();
}
//方式2:如果返回时是一个对象的时候会调用拷贝构造函数
Person myFun3()
{
static Person p(1001, "zhangsan", 'M', 90);
return p;
}
//注意:返回值是引用不会调用拷贝构造函数
Person &myFun4()
{
static Person p(1001, "zhangsan", 'M', 90);
return p;
}
void test2()
{
Person p1(1001, "zhangsan", 'M', 90);
myFun1(p1);
//myFun2(p1);
cout << "*****************" << endl;
Person p2 = myFun3();
p2.printMsg();
//以下形式也会调用拷贝构造函数,可以认为是一个匿名对象
//myFun3();
cout << "*****************" << endl;
Person &p3 = myFun4();//返回值是&,最好定义一个&来接收,不用&接收,相当于又开辟了空间,是不对的,因为返回&的目的就是为了不再开辟空间
p3.printMsg();
}
int main()
{
test2();
return 0;
}
执行结果:
test1:
test2:
2.4 当成员变量有指针时三个函数的编写
==当成员变量中有指针时,构造、析构和拷贝构造的书写==
#include <iostream>
#include <string.h>
using namespace std;
//当成员变量中有指针时,构造、析构和拷贝构造的书写
class Person{
public:
//构造函数
Person()//清空
{
cout << "无参构造函数" << endl;
p_id = 0;
p_name = NULL;
p_score = 0;
p_address = NULL;
}
Person(int id, char *name, int score, char *address)
{
cout << "有参构造函数" << endl;
p_id = id;
p_name = new char[strlen(name) + 1];
strcpy(p_name, name);
p_score = score;
p_address = new char[strlen(address) + 1];
strcpy(p_address, address);
}
//析构函数
//释放堆区的空间
//p1释放了但是,堆区开辟的p_name = new char[strlen(name) + 1];也需要释放,如果里面的成员是普通成员,是栈区开辟的,就不需要手动释放,
//p1的释放只是释放的是两个栈区的普通变量,堆区只是释放两个指针变量,空间没有释放
~Person()
{
cout << "析构函数" << endl;
if(p_name != NULL)//防止是无参构造,如果不等于NULL,就能释放空间了,因为释放只能是堆区的
{
delete []p_name;
p_name = NULL;
}
if(p_address != NULL)
{
delete []p_address;
p_address = NULL;
}
}
//拷贝构造函数
//拷贝构造函数里面需要对指针变量重新开辟空间,防止浅拷贝
Person(const Person &obj)
{
cout << "拷贝构造函数" << endl;
p_id = obj.p_id;
p_name = new char[strlen(obj.p_name) + 1];
strcpy(p_name, obj.p_name);
p_score = obj.p_score;
p_address = new char[strlen(obj.p_address) + 1];
strcpy(p_address, obj.p_address);
}
void printMsg()
{
cout << p_id << ", ";
cout << p_name << ", ";
cout << p_score << ", ";
cout << p_address << endl;
}
private:
int p_id;
char *p_name;
int p_score;
char *p_address;
};
void test1()
{
Person p1(1001, "张三", 90, "北京");
p1.printMsg();
Person p2 = p1;
p2.printMsg();
}
int main()
{
test1();
return 0;
}
2.5 拷贝赋值函数
2.5.1 格式
类名& operator=(const 类名 &obj)
{
if(this != &obj)
{
//成员之间的拷贝的逻辑
}
return *this;
}
2.5.2 调用时机
两个已经完成初始化的对象之间进行赋值的操作时会调用拷贝赋值函数
本质是重载了=运算符,从编译器的角度看 ----> s2.operator=(s1);
Student s1("小明",18);//有参构造
Student s2("小红",17);//有参构造
s2 = s1;//拷贝赋值函数