构造函数和析构函数
对象的初始化和清理是两个非常重要的安全问题
使用一个没有初始状态的对象,其使用结果是未知的
没有及时清理一个使用完毕的对象,也会造成安全问题
c++利用构造函数和析构函数解决上述问题,这两个函数会被编译器自动调用,完成对象的初始化和清理工作。对象的初始化和清理工作是编译器强制我们做的事情,因此如果我们不提供构造和析构函数,编译器会提供空实现的构造析构函数
构造函数:主要作用在于创建对象时为对象的成员属性赋值,构造函数由编译器自动调用,无需手动调用。
析构函数:主要作用在于对象销毁前系统自动调用,执行一些清理操作。
构造函数语法:
- 类名(){}
- 构造函数,没有返回值,不用写void
- 函数名称与类名相同
- 构造函数可以有参数,可以发生重载
- 程序在调用对象时会自动调用构造函数,无需手动调用且只会调用一次。
析构函数语法:
- ~类名(){}
- 析构函数,没有返回值,不同写void
- 函数名称与类名相同,在名称前加符号~
- 析构函数不可以有参数,因此不可以发生重载
- 程序在对象销毁前会自动调用析构函数,无需手动调用且只会调用一次
#include <iostream>
#include <stdlib.h>
using namespace std;
class Person
{
//构造函数和析构函数由系统在类外调用 要定义在public区域中
public:
Person()
{
cout << "Person构造函数调用" << endl;
}
~Person()
{
cout << "Person析构函数调用" << endl;
}
};
void text()
{
Person p; //存放在栈中的变量 在函数结束时 调用析构函数
// exit(1);
}
int main()
{
Person p;
text();
Person* pptr = new Person; //存放在堆区中的变量 在new时调用构造函数
delete pptr; //delete时调用析构函数
return 0;
}
从上面程序的执行结果可以看出来,构造函数和析构函数的调用时机和对象的生存期是密切相关的,实例化对象是调用构造函数,销毁对象时调用析构函数
构造函数的分类和调用
两种分类方式
- 按参数分为:有参构造和无参构造(默认构造)
- 按类型分为:普通构造和拷贝构造
三种调用方式
- 括号法 Person p(p1);
- 显示法 Person p = Person(p1);
- 隐式转换法 Person p = p1;
#include <iostream>
using namespace std;
class Person
{
public:
int p_age = 0;
public:
Person()
{
cout << "无参构造函数调用 age = " << p_age << endl;
}
Person(int age)
{
p_age = age;
cout << "有参构造函数调用 age = " << p_age << endl;
}
Person(const Person &p) //拷贝构造函数传入参数 为 const类的引用 避免改变被拷贝的对象 提高调用效率
{
p_age = p.p_age;
cout << "拷贝构造函数调用 age = " << p_age << endl;
}
~Person()
{
cout << "析构函数调用 age = " << p_age << endl;
}
};
void text()
{
Person p1;
Person p2(10);
Person p3(p2);
//注意 括号法的无参函数调用不可以写成
//Person p1(); 编译器会当成 函数声明 而非 对象声明
//显示法
Person p4 = Person(20);
Person p5 = Person(p4);
//括号右边称为 匿名对象声明 匿名对象生存期 为其声明行
cout << "匿名对象开始" << endl;
Person(30);
cout << "匿名对象结束" << endl;
//注意 不要用拷贝构造函数 初始化匿名对象
//Person(p5); 编译器会当成 Person p5 而出现重复定义错误
//隐式转换法
Person p6 = 40; //等价于Person p6 = Person(40);
Person p7 = p6;
}
int main()
{
text();
return 0;
}
需要注意的有两点:
- 括号法无法实现无参构造 Person p() 编译器会认为这是一个函数声明而非对象声明
- 匿名对象不能用拷贝构造函数初始化 Person(p)编译器会认为这时对象p的声明而产生重定义的错误。
拷贝构造函数调用时机
c++中拷贝构造函数调用时机通常有三种情况
- 使用一个已经创建完毕的对象来初始化一个新对象
- 值传递方式给函数参数传值
- 以值方式返回局部对象
#include <iostream>
using namespace std;
class Person
{
public:
int p_age;
Person()
{
cout << "无参构造函数调用" << p_age << endl;
}
Person(int age)
{
p_age = age;
cout << "有参构造函数调用" << p_age << endl;
}
Person(const Person &p)
{
p_age = p.p_age;
cout << "拷贝构造函数调用" << p_age << endl;
}
Person operator=(const Person& p)
{
p_age = p.p_age;
return *this;
}
~Person()
{
cout << "析构函数调用" << p_age << endl;
}
};
//拷贝构造函数调用的三个常见情况
//对象间的拷贝赋值
void text1()
{
Person p(10);
Person p1 = p;
}
//对象作为参数传入函数 实参与形参之间会出现拷贝赋值 调用拷贝函数
void doWork1(Person p)
{
return ;
}
void text2()
{
Person p2(20);
doWork1(p2);
}
//对象作为返回值返回 返回值被接受时出现拷贝赋值 调用拷贝函数
Person doWork2()
{
Person p3(30);
return p3;
}
void text3()
{
Person p4(40);
p4 = doWork2(); //这里返回值返回时被编译器优化没有出现拷贝赋值
}
int main()
{
text1();
text2();
text3();
return 0;
}
运行结果如下
需要注意的一点是,对象值返回时编译器会优化代码,不会调用拷贝构造函数
我这里重载了赋值运算符,可以看到对象值返回时调用了构造函数
构造函数调用时机
默认情况下,c++编译器至少给一个类添加三个函数
- 默认构造函数(无参,函数体为空)
- 默认拷贝构造函数,对属性进行值拷贝
- 默认析构函数(无参,函数体为空)
构造函数调用规则如下
- 如果用户定义有参构造函数,c++不在提供默认无参构造
- 如果用会定义拷贝构造函数,c++不会再提供其他构造函数
#include <iostream>
using namespace std;
class Person0
{
public:
int p_age;
~Person0()
{
cout << "Person0析构函数调用 age = " << p_age << endl;
}
};
class Person1
{
public:
int p_age;
Person1()
{
cout << "Person1无参构造函数调用 age = " << p_age << endl;
}
~Person1()
{
cout << "Person1析构函数调用" << endl;
}
};
class Person2
{
public:
int p_age;
Person2(int age)
{
p_age = age;
cout << "Person2有参构造函数调用 age = " << p_age << endl;
}
~Person2()
{
cout << "Person2析构函数调用" << endl;
}
};
class Person3
{
public:
int p_age;
Person3(const Person3& p)
{
p_age = p.p_age;
cout << "Person3拷贝构造函数调用 age = " << p_age << endl;
}
~Person3()
{
cout << "Person3析构函数调用" << endl;
}
};
void text0()
{
Person0 p00;
p00.p_age = 18;
cout << "p00.age = " << p00.p_age << endl;
// Person0 p01(18); //编译器提供的有参构造函数为空实现函数 不能实现有参构造
Person0 p02(p00); //编译器会提供拷贝构造函数实现 值拷贝
cout << "p02.age = " << p02.p_age << endl;
}
void text1()
{
Person1 p10; //自己添加的默认构造函数回覆盖系统的空函数
p10.p_age = 28;
// Person1 p11(28); //与Person0同理
Person1 p12(p10); //调用编译器提供的拷贝构造函数
cout << "p12.age = " << p12.p_age << endl;
}
void text2()
{
// Person2 p20; //当类中有有参函数构造 编译器不会提供默认构造函数
Person2 p21(38);
Person2 p22(p21);
cout << "p22.age = " << p22.p_age << endl;
}
void text3()
{
// Person3 p30;
// Person3 p31(48);
// Person3 p32(p31); //当类中有拷贝构造函数 编译器不会提供默认构造函数和有参函数构造
}
int main()
{
text0();
text1();
text2();
text3();
return 0;
}
静态成员
静态成员就是在成员变量和成员函数前加上static关键字,称之为静态成员。
静态成员分为:
- 静态成员变量
- 所有对象共享一份数据
- 在编译阶段分配内存
- 类内声明,类外初始化
- 静态成员函数
- 所有对象共享一个函数
- 静态成员函数只能访问静态成员变量。
静态成员变量
#include <iostream>
using namespace std;
//总结:类中的静态成员变量是对于类 来说的不属于任何一个对象 修改静态成员变量 通过类实例化的所有对象的静态成员变量都会改变
class Person
{
public:
static int m_a;
private:
static int m_b;
};
//访问静态成员变量之前必须初始化 区分于一般的静态变量
int Person::m_a = 100; //类外初始化格式 表示是在对Person作用域中的变量赋值
int Person::m_b = 200;
void text()
{
Person p1;
Person p2;
//访问静态变量的两种方式:
//1:通过对象访问
p2.m_a = 200; //通过对象p2也可以修改对象p1的静态变量
cout << "a = " << p1.m_a << endl;
//2:通过类名加作用域访问
Person::m_a = 300;
cout << "a = " << Person::m_a << endl;
// cout << "b = " << p1.m_b << endl; //类中的静态变量也有访问权限
}
int main()
{
text();
return 0;
}
在上面的代码中需要我们注意的有三点
- 静态成员变量编译时在内存中的全局区分配内存空间,因此必须在全局作用域下初始化静态成员变量 初始化格式 int Person:: m_A = 10(变量类型 类名:: 变量名 = 10)
- 由于所有的成员变量共享一份数据,所以静态成员变量除了通过实例化的对象访问也可以直接通过类名进行访问 访问格式: Person::m_A
- 静态成员变量也是有访问权限的。
静态成员函数
#include <iostream>
using namespace std;
class Person
{
public:
static void func0()
{
// m_b = 100; //静态成员函数只能访问静态成员变量 不能访问普通变量
m_a = 100;
cout << "func函数调用 a = " << m_a << endl;
}
private:
static int m_a;
int m_b;
//静态成员变量也有访问权限
static void func1()
{
;
}
};
int Person:: m_a = 0;
void text()
{
Person p;
//调用静态成员函数的两种方式
p.func0();
// 1:通过对象访问
// Person::func1(); //private不能在类外访问
// 2:通过类名访问
}
int main()
{
text();
return 0;
}
通过静态成员函数的代码需要我们注意的也有三点:
- 静态成员函数位于所有对象之外,只能操作类中的静态成员变量,不能操作类中非静态成员变量。
- 与静态成员变量相同,静态成员函数也有两种访问方式,通过对象访问、通过类名进行访问
- 静态成员函数也是有访问权限的
成员变量和成员函数分开存储
在c++中,类内的成员变量和成员函数分开存储
只有非静态成员才属于类的对象上
我们先来看看一个没有任何成员的类大小是多少
#include <iostream>
using namespace std;
class Person0
{
};
void text()
{
Person0 p0;
cout << sizeof(p0) << endl;
}
int main()
{
text();
return 0;
}
通过运行上面的代码,我们可以知道,空对象占用的内存空间大小为1,c++编译前会给每个空对象也分配一个字节占用块空间,来区分不同空对象占内存的位置,每个空对象都有一个独一无二的内存地址。
现在我们给空类添加一个非静态成员变量看看这时类的大小
#include <iostream>
using namespace std;
class Person0
{
};
class Person1
{
int m_a;
};
void text()
{
Person0 p0;
Person1 p1;
cout << "size of Person0 = " << sizeof(p0) << endl;
cout << "size of Person1 = " << sizeof(p1) << endl;
}
int main()
{
text();
return 0;
}
通过运行上面的代码我们可以看到,类的大小变为了四,所以非静态成员变量属于类的对象上,我们再来给类加上静态成员变量和非静态成员函数。
#include <iostream>
using namespace std;
class Person0
{
};
class Person1
{
int m_a;
};
class Person2
{
int m_a;
static int m_b;
void funca() {}
static void funcb() {}
};
int Person2:: m_b = 0;
void text()
{
Person0 p0;
Person1 p1;
Person2 p2;
cout << "size of Person0 = " << sizeof(p0) << endl;
cout << "size of Person1 = " << sizeof(p1) << endl;
cout << "size of Person2 = " << sizeof(p2) << endl;
}
int main()
{
text();
return 0;
}
运行结果没有改变,仍然是 4 ,这是因为静态成员变量和成员函数并不属于某个对象上,而是同类型的对象公用一个静态变量和而非静态函数,同理,静态成员函数也不属于某一个对象。
综上有两个要点:
- 空对象占用内存空间为 1
- 处理非静态成员变量,其他成员均不属于类的对象上。
深拷贝与浅拷贝
深浅拷贝是面试的一个经典问题,也是一个常见的坑
浅拷贝:简单的复制拷贝操作
深拷贝:在堆区重新申请空间,进行重新拷贝操作
#include <iostream>
using namespace std;
class Person
{
public:
int p_age;
int* p_ptrhight;
Person(int age, int hight)
{
p_age = age;
p_ptrhight = new int(hight);
cout << "有参构造函数调用" << endl;
}
Person(const Person& p)
{
p_age = p.p_age;
// p_ptrhight = p.p_ptrhight; //编译器的拷贝构造函数类似于这样 是单纯的值赋值 属于浅拷贝
p_ptrhight = new int(*p.p_ptrhight); //解决重复回收的问题要将浅拷贝换为深拷贝
//在栈区中再申请一块内存避免重复回收
}
~Person()
{
if(!p_ptrhight)
{
delete p_ptrhight;
p_ptrhight = NULL;
}
cout << "析构函数调用" << endl;
}
};
void text()
{
Person p1(18, 160);
cout << "p1的年龄为:" << p1.p_age << "p1的身高为:" << *p1.p_ptrhight << endl;
Person p2(p1); //编译器提供的拷贝构造函数是一个字节一个字节的完全拷贝 拷贝的指针也是相同的
cout << "p2的年龄为:" << p2.p_age << "p2的身高为:" << *p2.p_ptrhight << endl;
//由于两个对象的ptrhight指针指向同一块内存 使得调用析构函数时会重复回收内存 是非法的
//栈区中的数据存在系统栈中 先进后出 会先回p2再回首p1
}
int main()
{
text();
return 0;
}
当对象有成员创建在堆区时,两个对象之间的直接赋值只会将堆区空间的指针赋值,这样两个对象成员就只想了同一块内存,当两个对象执行析构函数时,这个堆区空间就会被释放两次。
这时我们就需要用深拷贝,在赋值时重新申请一块新的堆区空间。
初始化列表
作用:c++提供了初始化列表的语法,用来初始化属性。
语法: 构造函数():属性1(值1),属性2(值2)……{};
#include <iostream>
using namespace std;
class Person
{
private:
int p_a;
int p_b;
int p_c;
public:
//在函数体中实现属性的赋值
// Person(int a, int b, int c)
// {
// p_a = a;
// p_b = b;
// p_c = c;
// }
// //可以直接在列表中给属性付常量值
// Person(): p_a(10), p_b(20), p_c(30)
// {
// ;
// }
//先将值赋给形参传入函数栈区 再在列表中给属性赋值
Person(int a, int b, int c): p_a(a), p_b(b), p_c(c)
{
;
}
Show()
{
cout << "p_a = " << p_a << endl;
cout << "p_b = " << p_b << endl;
cout << "p_c = " << p_c << endl;
}
};
void text()
{
Person p(10, 20, 30);
p.Show();
}
int main()
{
text();
return 0;
}
类对象作为类成员
c++类中的成员可以使另一个类的对象,我们称该成员为 对象成员
例如
class A{};
class B
{
A a;
}
B类中有对象A作为成员,A作为对象成员
那么当创建B对象时,A与B的构造和析构顺序谁先谁后?
#include <iostream>
#include <string>
using namespace std;
class Phone
{
public:
string m_name;
Phone(string name): m_name(name)
{
cout << "Phone构造函数调用" << endl;
}
~Phone()
{
cout << "Phone析构函数调用" << endl;
}
};
class Person
{
public:
string m_name;
Phone m_phone;
Person(string name, string pname): m_name(name), m_phone(pname) //初始化列表对person类中成员赋值
{ //m_phone(pname)可以看成phone类的括号法的有参构造函数调用
cout << "Person构造函数调用" << endl;
}
Show()
{
cout << m_name << "拿着" << m_phone.m_name << endl;
}
~Person()
{
cout << "Person析构函数调用" << endl;
}
};
void text()
{
string name, pname;
cin >> name >> pname;
Person p(name, pname);
p.Show();
}
int main()
{
text();
return 0;
}
程序运行结果如下:
我们得到以下结论:
- 当其它类对象对为本类成员,构造时先构造其它类对象,在构造自身。
- 析构的顺序与构造顺序相反。
this指针与链式编程思想
成员变量和成员函数是分开存储的
每一个非静态成员函数只会诞生一份函数实例,也就是说多个同类型的对象会公用一块函数代码
那么问题是:这一块代码如何分辨是哪一个对象调用的自己呢,如果不能分别调用对象我们在函数中如何使用该对象的数据呢?
c++提供了一个特殊的对象指针 this指针,来解决上述问题,this指针指向调用当前成员函数的对象。
- this指针隐含在每一个非静态成员函数内
- this指针不需要定义,直接使用即可。
- his指针的用途:
- 当形参和成员变量同名时,可以用this指针来区分
- 当类的非静态成员函数要返回对象本身时,可以return *this;
#include <iostream>
using namespace std;
//this:指向这个对象的一个指针
class Person
{
public:
int age;
//this 指针用处一:当形参与成员变量重名时区分二者
Person(const Person& p)
{
cout << "Person构造函数调用" << endl;
}
Person(int age)
{
this -> age = age;
}
Person& PersonAddAge(const Person& p)
// Person PersonAddAge(const Person& p) 值传递
{
this -> age += p.age;
return *this;
}
};
void text()
{
Person p1(10);
Person p2(10);
//this指针用处二:返回对象本身
p1.PersonAddAge(p2).PersonAddAge(p2).PersonAddAge(p2); //返回值定义为引用类型 否则返回一个新的对象 变为拷贝构造函数的重复调用
//链式编程
cout << p1.age << endl;
}
int main()
{
text();
return 0;
}
在上面代码中,如果将PersonAddAge返回值改为Person类型,那么p1.age就不会改变,原因在于,PersonAddAge从引用返回变为了单纯的值返回,返回了一个新的Person对象,而没有继续改变p1的年龄。
p1.PersonAddAge(p2).PersonAddAge(p2).PersonAddAge(p2)语句中,每调用一次成员函数都会返回Person类型对象,这样的调用可以无限追加下去,这种编程的思想叫做链式编程。
cout,cin输入输出流用左移右移运算符不断追加也是这种编程思想。
空指针访问成员函数
由于成员函数存在于所有类对象之外,我们可以用一个没有指向任何对象的空指针调用成员函数。
但与之而来的问题是在成员函数中访问成员变量会调用this指针,由于当前没有实例化的对象所以this为空,这时在访问成员变量就会报错
因此在成员函数中如果用到this指针,需要加以判断保证代码的健壮性。
#include <iostream>
using namespace std;
class Person
{
public:
int m_age;
void ShowName()
{
cout << "Person" << endl; //没有调用this指针不会报错
}
void ShowAge()
{
if(!this) //指针访问成员变量时要判断是不是空指针
return ;
cout << m_age << endl;
}
};
int main()
{
Person* pptr = NULL;
pptr -> ShowName();
pptr -> ShowAge();
return 0;
}
常函数与常对象
常函数:
- 成员函数后加const后称之为常函数 void func() const
- 常函数内不可以修改成员属性
- 成员属性声明时加关键字mutable后,才能在常函数中修改 multable int m_a;
常对象:
- 声明对象前加const称该对象为常对象
- 常对象只能调用常函数
#include <iostream>
using namespace std;
class Person
{
public:
int m_a;
mutable int m_b; //成员变量前加上mutable修饰 使得成员变量可以在const成员函数中被修改
void func0() const //const成员函数中 不能对成员变量进行修改,相当于给每个函数中的变量加上了一个const前缀
{
// m_a = 100;
m_b = 100;
}
void func1()
{
m_a = 100;
m_b = 100;
}
};
void text()
{
Person p0;
p0.func0();
p0.func1();
p0.m_a = 100;
p0.m_b = 100;
//const修饰的对象必须初始化
const Person p1(p0); //const修饰的对象 不能修改成员变量的值 也不能访问非const成员函数
p1.func0(); //只能访问const成员函数
// p1.func1(); //
// p1.m_a = 100;
// p1.m_b = 100;
}
int main()
{
text();
return 0;
}
类中的this指针本质上是一个指针常量,不能指向其他数据。在成员函数后加const变为常函数后,相当于将Perons * const this 变为const Person * const this,除了指针常量外this指针在成员函数中还是一个常量指针,不能通过这个指针修改数据。
const Person p;在对象前加const,变为常对象,常对象只能调用常函数。
在成员变量声明前加关键字multable,使得改变量在常函数和常对象中能被修改。