目录
对象的初始化和清理
简单来说,对象构建的函数,根据需求可能会在内存区开辟空间,存储数据,如果不进行清理,那么下一次打开,就可能通过其他意外直接看到这些数据。
就像出售自己的二手手机,如果出售,在出售前必定清除数据,保证数据不会被其他人看到。
初始化和清理是必须做,可以由系统编译器去做,但是自己写了,就有限运行自己写的清理函数。
构造函数和析构函数
对象的初始化和清理也是两个非常重要的安全问题
一个对象或者变量没有初始状态,对其使用后果是未知
同样的使用完一个对象或变量,没有及时清理,也会造成一定的安全问题
c++利用了构造函数和析构函数解决上述问题,这两个函数将会被编译器自动调用,完成对象初始化和清理工作。
对象的初始化和清理工作是编译器强制要我们做的事情,因此如果我们不提供构造和析构,编译器会提供
请注意,编译器提供的构造函数和析构函数是空实现。
-
构造函数:主要作用在于创建对象时为对象的成员属性赋值,构造函数由编译器自动调用,无须手动调用。
-
析构函数:主要作用在于对象销毁前系统自动调用,执行一些清理工作。
构造函数语法:类名(){}
-
构造函数,没有返回值也不写void
-
函数名称与类名相同
-
构造函数可以有参数,因此可以发生重载
-
程序在调用对象时候会自动调用构造,无须手动调用,而且只会调用一次
class person
{
public://需要在公共权限下,才能访问到,这是因为class默认为私有权限,private
//1.1、构造函数
// 没有返回值,不用谢void
// 函数名 与类名相同
// 构造函数可以有参数,可以发生重载
// 创建对象的时候,构造函数会自动调用,而且只调用一次。
//构造函数语法: 类名(){}
person()
{
cout << "person构造函数的调用" << endl;
}
};
void test01()
{
person p;//请注意,这里只是调用了一个对象person的变量,未做其任何动作。
}
请注意,只是调用了一个对象person的变量,未做其任何动作,依旧成功输出了构造函数,说明,构造函数在c++内是自动调用的。
析构函数语法: ~类名(){}
-
析构函数,没有返回值也不写void
-
函数名称与类名相同,在名称前加上符号 ~
-
析构函数不可以有参数,因此不可以发生重载
-
程序在对象销毁前会自动调用析构,无须手动调用,而且只会调用一次
~person()
{
cout << "~person析构函数的调用" << endl;
}
同上,我们可以发现,只要调用person里的数据,就必定运行构造函数和析构函数。
如果系统的编写,往往是一个空实现,但是自己编写,则更加彻底和清晰。
总结:
1、构造函数相当于初始化,析构函数就是把class内数据删除干净,防止数据错误。
2、析构函数和构造函数,自己不写,系统提供,但不显示。
构造函数的分类及调用
两种分类方式:
按参数分为: 有参构造和无参构造
按类型分为: 普通构造和拷贝构造
有参构造和无参构造
class person
{
public://需要在公共权限下,才能访问到,这是因为class默认为私有权限,private
//构造函数
person()//无参
{
cout << "person的 无参 构造函数的调用" << endl;
}
person( int a)//有参
{
cout << "person的 有参 构造函数的调用" << endl;
}
};
拷贝构造函数
拷贝构造
//拷贝构造函数
//普通构造和拷贝构造
person(const person &p)//请注意,const禁止修改,并使用引用的写法,这是固定写法
{
age = p.age;
cout << "拷贝构造函数!" << endl;
}
其他的都是普通构造
三种调用方式:
括号法
void test01() //括号法调用
{
person p;//请注意,这里只是调用了一个对象person的变量,未做其任何动作。
person p2(10);//有参构造函数调用
person p3(p2);
//注意事项:调用默认构造函数的时候,不要加()
//因为语法规定,编译器认为这是一个函数的声明
cout << "p2的年龄为:: " << p2.age << endl; //给函数做了一个初始化
cout << "p3的年龄为:: " << p3.age << endl; //拷贝p2的数据
}
显示法
//显示法
person p1;
person p2 = person(10);
person p3 = person(p2);
//相较于括号调用,显示调用就是明显显示其关系,更加直观
person(10);//匿名对象 特点:当前执行结束后,系统会理解回收匿名对象
//匿名函数被调用后会被立即析构。
//不要利用拷贝构造函数,初始化匿名对象,会报错,因为编译器认为这是一个声明
隐式转换法
//隐式转换发
person p4 = 10; // 编译器自动给你转换成 person p4 = Person(10);
person p5 = p4; // Person p5 = Person(p4);
本小节代码
#include <iostream>
#include<string>
using namespace std;
//1、构造函数的分类及调用
//分类
//按照参数分类 无参构造和有参
class person
{
public:
//构造函数
person()//无参
{
cout << "person的 无参 构造函数的调用" << endl;
}
person( int a)//有参
{
age = a;
cout << "person的 有参 构造函数的调用" << endl;
}
//按照参数分类 无参构造和有参
//拷贝构造函数
//普通构造和拷贝构造
person(const person &p)//请注意,const禁止修改,并使用引用的写法,这是固定写法
{
age = p.age;
cout << "拷贝构造函数!" << endl;
}
public://需要在公共权限下,才能访问到,这是因为class默认为私有权限,private
int age;
};
void test01()
{
//括号法调用
//person p;//请注意,这里只是调用了一个对象person的变量,未做其任何动作。
//person p2(10);//有参构造函数调用
//person p3(p2);
注意事项:调用默认构造函数的时候,不要加()
因为语法规定,编译器认为这是一个函数的声明
//cout << "p2的年龄为:: " << p2.age << endl; //给函数做了一个初始化
//cout << "p3的年龄为:: " << p3.age << endl; //拷贝p2的数据
//显示法
//person p1;
//person p2 = person(10);
//person p3 = person(p2);
相较于括号调用,显示调用就是明显显示其关系,更加直观
//person(10);//匿名对象 特点:当前执行结束后,系统会理解回收匿名对象
匿名函数被调用后会被立即析构。
不要利用拷贝构造函数,初始化匿名对象,会报错,因为编译器认为这是一个声明
//隐式转换发
person p4 = 10; // 编译器自动给你转换成 person p4 = Person(10);
person p5 = p4; // person p5 = Person(p4);
}
int main()
{
test01();
system("pause");
return 0;
}
拷贝构造函数的调用时机
C++中拷贝构造函数调用时机通常有三种情况
-
使用一个已经创建完毕的对象来初始化一个新对象
-
值传递的方式给函数参数传值
-
以值方式返回局部对象
使用一个已经创建完毕的对象来初始化一个新对象
效果如图,可以调用p2,直接使用p1的数据。
代码
class person
{
public:
person()
{
cout << "person的默认构造函数调用 " << endl;
}
person(int age)
{
cout << "person的有参构造函数调用 " << endl;
m_age = age;
}
person(const person & p) //拷贝构造函数
{
cout << "person的默认构造函数调用 " << endl;
m_age = p.m_age;
//注意用法,这里是将m_age=p.m_age,
//相当于使用一个已经创建完毕的对象来初始化一个新对象
}
~person()
{
cout << "person的析构函数调用 " << endl;
}
int m_age;
};
void test01()
{
person p1(20);
person p2(p1); //括号法调用拷贝构造函数
cout << "p2的年龄为: " << p2.m_age << endl;
}
int main()
{
test01();
system("pause");
return 0;
}
值传递的方式给函数参数传值 ![](https://i-blog.csdnimg.cn/blog_migrate/77364a417d9e736ea356f796ad42bf88.png)
请注意,代码段并不完整,不要盲目粘贴
//2、值传递的方式给函数参数传值
void do_work(person p)//值传递,没有&,看清楚啊
{
}
void test02()
{
person p;
do_work(p);
}
int main()
{
//test01();
test02();
以值方式返回局部对象
//3、以值方式返回局部对象
person do_work2()
{
person p1; //调用person数据
return p1; //再返回p1
}
void test03()
{
person p = do_work2();//吧p赋值为do_work2
}
int main()
{
//test01();
//test02();
test03();
本节 完整代码
#include <iostream>
#include<string>
using namespace std;
//C++中拷贝构造函数调用时机通常有三种情况
//
//* 使用一个已经创建完毕的对象来初始化一个新对象
//* 值传递的方式给函数参数传值
//* 以值方式返回局部对象
class person
{
public:
person()
{
cout << "person的默认构造函数调用 " << endl;
}
person(int age)
{
cout << "person的有参构造函数调用 " << endl;
m_age = age;
}
person(const person & p) //拷贝构造函数
{
cout << "person的默认构造函数调用 " << endl;
m_age = p.m_age;
//注意用法,这里是将m_age=p.m_age,
//相当于使用一个已经创建完毕的对象来初始化一个新对象
}
~person()
{
cout << "person的析构函数调用 " << endl;
}
int m_age;
};
void test01()
{
person p1(20);
person p2(p1); //括号法调用拷贝构造函数
cout << "p2的年龄为: " << p2.m_age << endl;
}
//2、值传递的方式给函数参数传值
void do_work(person p)//值传递,没有&,看清楚啊
{
}
void test02()
{
person p;
do_work(p);
}
//3、以值方式返回局部对象
person do_work2()
{
person p1; //调用person数据
return p1; //再返回p1
}
void test03()
{
person p = do_work2();//吧p赋值为do_work2
}
int main()
{
//test01();
//test02();
test03();
system("pause");
return 0;
}
构造函数调用规则
只要写了类,必定会有默认构造函数、拷贝构造函数、析构函数
你可以不写,系统提供,你写了,系统就用你的,不会提供别的构造函数。
默认情况下,c++编译器至少给一个类添加3个函数
1.默认构造函数(无参,函数体为空)
2.默认析构函数(无参,函数体为空)
3.默认拷贝构造函数,对属性进行值拷贝
构造函数调用规则如下:
-
如果用户定义有参构造函数,c++不在提供默认无参构造,但是会提供默认拷贝构造
-
如果用户定义拷贝构造函数,c++不会再提供其他构造函数
无参构造拷贝
有参构造
不太理解,但是规则就是这么个回事
如果用户提供有参构造,编译器不会提供默认构造,会提供拷贝构造
如果用户提供拷贝构造,编译器不会提供其他构造函数
我不认为这是重点,但这是一个语法的运用规则,
#include <iostream>
#include<string>
using namespace std;
//构造函数调用规则
//默认情况下,c++编译器至少给一个类添加3个函数
//1.默认构造函数(无参,函数体为空)
//2.默认析构函数(无参,函数体为空)
//3.默认拷贝构造函数,对属性进行值拷贝
class person
{
public:
person()
{
cout << "person的无——参构造函数" << endl;
}
person(int age)
{
m_age = age;
cout << "person的有——参构造函数" << endl;
}
person(const person& p)
{
m_age = p.m_age;
cout << "person的拷贝构造函数" << endl;
}
~person()
{
cout << "person的析构构造函数" << endl;
}
int m_age;
};
//void test01()
//{
// person p;
// p.m_age = 18;
//
// person p2(p);//拷贝构造的用法
// cout << "p2的年龄为: " << p2.m_age << endl;
//}
void test02()
{
//如果用户提供有参构造,编译器不会提供默认构造,会提供拷贝构造
person p1; //此时如果用户自己没有提供默认构造,会出错
person p2(10); //用户提供的有参
person p3(p2); //此时如果用户没有提供拷贝构造,编译器会提供
//如果用户提供拷贝构造,编译器不会提供其他构造函数
person p4; //此时如果用户自己没有提供默认构造,会出错
person p5(10); //此时如果用户自己没有提供有参,会出错
person p6(p5); //用户自己提供拷贝构造
}
int main()
{
test02();
return 0;
}
深拷贝与浅拷贝(重点)
这里是个面试重点,因为这是坑
浅拷贝:简单的赋值拷贝操作
深拷贝:在堆区重新申请空间,进行拷贝操作
这里就是浅拷贝了
深拷贝和浅拷贝区别是,深拷贝的数据在堆区,只能有程序员手动编写代码释放。
这里就体现析构函数的用法。
这里是用一个判断,首先判断,m_high是否为空,为空就释放,但是p2是拷贝p1的,内存地址一致,因此第一遍将p1释放后,在释放p2,可是内存地址已经不在了,就出现了错误。
于是要我们自己写拷贝函数中的深拷贝操作,来解决浅拷贝带来的重复释放堆区问题
完整代码
#include <iostream>
#include<string>
using namespace std;
//浅拷贝:简单的赋值拷贝操作
//深拷贝:在堆区重新申请空间,进行拷贝操作
class person
{
public:
person()
{
cout << "person的默认构造函数调用" << endl;
}
person(int age,int high)
{
m_age = age;
m_high = new int(high);
//利用new,把high指向堆区,返回一个int* ,然后用一个int*去接受
cout << "person的有参构造函数调用" << endl;
}
//自己写一个拷贝构造函数,解决浅拷贝带来的问题
person(const person& p)
{
cout << "拷贝构造函数!" << endl;
//如果不利用深拷贝在堆区创建新内存,会导致浅拷贝带来的重复释放堆区问题
m_age = p.m_age;
//m_high=p.m_high 就是编译器默认实现的代码
//因此我们需要自己写拷贝函数中的深拷贝操作
m_high = new int(*p.m_high);
}
//~person()
//{
// cout << "person的析构函数调用" << endl;
//}
//析构函数
~person() {
cout << "析构函数!" << endl;
if (m_high != NULL)
{
delete m_high;
}
}
int m_age;
//注意,整活了,使用指针,把身高的数据指向一片空间,堆区
int * m_high;
};
void test01()
{
person p1(18,180);
cout << "p1的年龄为: " << p1.m_age <<"身高为: "<<*p1.m_high << endl;
person p2(p1);
cout << "p2的年龄为: " << p2.m_age << "身高为: " <<* p2.m_high << endl;
}
int main()
{
test01();
return 0;
}
如果属性有在堆区开辟的,一定要自己提供拷贝构造函数,防止浅拷贝带来的问题
初始化列表
C++提供了初始化列表语法,用来初始化属性
语法:构造函数():属性1(值1),属性2(值2)... {}
构造函数就是一种初始化方法,同时有另外一种更为简单的初始化,初始化列表
相较于传统的,列表耿局简单,明了的特点。
class person
{
public:
//传统版
//person(int a, int b, int c)
//{
// m_a = a;
// m_b = b;
// m_c = c;
//}
//列表版
//初始化列表方式初始化
person(int a, int b, int c) :m_a(a), m_b(b), m_c(c) {}
void PrintPerson()
{
cout << "mA:" << m_a << endl;
cout << "mB:" << m_b << endl;
cout << "mC:" << m_c << endl;
}
private:
int m_a;
int m_b;
int m_c;
};
void test01()
{
person p1(18,20,24);
//传统版 ,在public:权限下
/*cout << "m_a=" << p1.m_a << " " << "m_b=" << p1.m_b
<< " " << "m_c=" << p1.m_c<< endl;*/
//列表版,加上private的
p1.PrintPerson();
}
int main()
{
test01();
return 0;
}
类对象作为类成员
C++类中的成员可以是另一个类的对象,我们称该成员为 对象成员
简单理解就是嵌套,和struct一样的,套娃。但是要注意权限的问题,class最麻烦的就是权限之间的问题。
练习
要注意变量名,建议纸上画好关系图,再来命名,不然容易蒙。
当其他类的成员为本类成员,优先构造其他类的对象,在构造自身
析构则相反,
#include <iostream>
#include<string>
using namespace std;
class phone //手机类
{
public:
phone(string p_name)
{
m_p_name = p_name;
}
//品牌名称
string m_p_name;
};
class user //用户
{
public:
//phoen m_p_name=p_name .隐式转换法,有系统代替完成
user(string m_uname,string p_name ):m_user_name(m_uname),m_phone(p_name)
{
cout << "user的构造函数调用" << endl;
}
~user()
{
cout << "user的析构函数调用" << endl;
}
void play_phone()
{
cout << m_user_name << " 使用" << m_phone.m_p_name << " 牌手机! " << endl;
}
//姓名
string m_user_name;
//手机
phone m_phone;
};
void test01()
{
user u("张三", "华为");
u.play_phone();
}
int main()
{
test01();
return 0;
}
静态成员
静态成员就是在成员变量和成员函数前加上关键字static,称为静态成员
静态成员分为:静态成员变量 静态成员函数
-
静态成员变量
-
所有对象共享同一份数据
-
在编译阶段分配内存
-
类内声明,类外初始化
-
-
静态成员函数
-
所有对象共享同一个函数
-
静态成员函数只能访问静态成员变量
-
静态成员变量
-
所有对象共享同一份数据
-
在编译阶段分配内存
-
类内声明,类外初始化
在类外初始化后就可以正常调用了
要加上域,告诉编译器属于谁
重新对静态成员变量赋值之后,还是打印p1,就会发现值变了。
静态成员变量,不属于某个对象上,所有对象都共享同一份数据
因此静态成员变量有两种访问方式
补充:类外不可以访问私有权限,仅能通过公共权限的接口
静态成员函数
大体上和静态变量一致。
两种访问方式
请注意,静态函数只能访问静态变量
如图
正确用法。
不可以访问的原因是因为无法区别变量是那个对象的,置多个或嵌套的对象
#include <iostream>
#include<string>
using namespace std;
class person
{
public:
特性1:所有对象都共享一份数据,
//特性2:编译阶段就分配了内存
// 类内声明,类外初始化,就像int a;
//静态函数
//与静态变量,一样
static void func()
{
m_a = 100;
//m_b = 200;非静态,禁止访问,
//不可以访问的原因是因为无法区别那个对象的,置多个或嵌套的对象
cout << "static void func静态函数调用" << endl;
}
static int m_a;
int m_b;
};
//类外初始化
int person::m_a;
//要加上域,告诉编译器属于谁
void test01()
{
//静态成员变量,不属于某个对象上,所有对象都共享同一份数据
//因此静态成员变量有两种访问方式
//1、通过对象访问
//person p1;
//p1.m_a = 10;
//cout << p1.m_a << endl;
//person p2;
//p2.m_a = 200; //给静态成员变量重新赋值后
//cout << p1.m_a << endl;
//2、通过类名访问
cout << person::m_a << endl;
//静态函数的调用和静态变量一致,都是通过两种方式,1,、对象访问,2、类名访问
//1 、对象访问
person p3;
p3.func();
//2、类名访问
person::func();
}
int main()
{
test01();
return 0;
}
C++对象模型和this指针
成员变量和成员函数分开存储
在C++中,类内的成员变量和成员函数分开存储
只有非静态成员变量才属于类的对象上
class Person {
public:
Person()
//空对象会占用内存空间: 1
//c++编译器会给每一个空对象分配一个自己空间,是为了区分空对象
//每个空对象都有独特的内存空间。
{
mA = 0;
}
//非静态成员变量占对象空间
int mA;
//静态成员变量不占对象空间
static int mB;
//函数也不占对象空间,所有函数共享一个函数实例
void func() {
cout << "mA:" << this->mA << endl;
}
//静态成员函数也不占对象空间
static void sfunc() {
}
};
int main() {
cout << sizeof(Person) << endl;
system("pause");
return 0;
}
没什么好讲的,这一部分更多讲原理,方便下面内容的理解,重点就一句,类内成员变量和成员函数是分开存储的,这导致为了提高算法效率,要注意程序的设计。
this指针概念
建议先读一段话,重点理解一下内容。
有点抽象,需要图解。
C++中成员变量和成员函数是分开存储的
每一个非静态成员函数只会诞生一份函数实例,也就是说多个同类型的对象会共用一块代码
那么问题是:这一块代码是如何区分那个对象调用自己的呢?
c++通过提供特殊的对象指针,this指针,解决上述问题。this指针指向被调用的成员函数所属的对象
this指针是隐含每一个非静态成员函数内的一种指针
this指针不需要定义,直接使用即可
this指针的用途:
-
当形参和成员变量同名时,可用this指针来区分
-
在类的非静态成员函数中返回对象本身,可使用return *this
·
但是不建议这么干,同样的变量名他人阅读的时候脑袋会炸的,多敲敲键盘。
这就使得,p2可以反复多次调用p1.
这种就是链式编程思想
#include <iostream>
#include<string>
using namespace std;
class Person
{
public:
Person(int age)
{
//1、当形参和成员变量同名时,可用this指针来区分
this->age = age;
}
Person& PersonAddPerson(Person p)
{
this->age += p.age;
//返回对象本身,对象本身是int,还返回int,这是个重点
return *this;
}
int age;
};
void test01()
{
Person p1(10);
cout << "p1.age = " << p1.age << endl;
Person p2(10);
//在第一次调用返回的是void,因此下一次重复调用,不符合调用要求。拒绝调用
p2.PersonAddPerson(p1).PersonAddPerson(p1).PersonAddPerson(p1);
cout << "p2.age = " << p2.age << endl;
}
int main() {
test01();
system("pause");
return 0;
}
空指针访问成员函数
C++中空指针也是可以调用成员函数的,但是也要注意有没有用到this指针
如果用到this指针,需要加以判断保证代码的健壮性
本节重点是思想,尽量避免程序的bug
看到这里就知道哪里出了问题。
关闭p->show_person_age();,则能正常运转。很奇妙
有点好奇,,有必要每个指针都要写一个if来保证嘛?
class person
{
public:
void print_class_name()
{
cout << "person class doing" << endl;
}
void show_person_age()
{
//处理办法
if (this == NULL)
{
return;//给返回,就不会到下面一行了
}
cout << "person年龄age= " <<this-> m_age << endl;
}
int m_age;
};
void test01()
{
person* p = NULL;//因这里的指针已经指向了空,所以在调用时就报错了、
p->print_class_name();
p->show_person_age();
}
int main()
{
test01();
system("pause");
return 0;
}
const修饰成员函数
常函数:
-
成员函数后加const后我们称为这个函数为常函数
-
常函数内不可以修改成员属性
-
成员属性声明时加关键字mutable后,在常函数中依然可以修改
就是简单的锁定,但要知道用法
但是加上const后
在语法上,在成员变量后加上const后,就禁止修改了
但是加上mutable就是可以修改的
class person
{
public:
//this指针的本质,是指针常量,指针的指向是不可以修改的
//const person *const this ---双重锁定,值也不可以修改
//在成员函数后面加上const ,修饰的是this指向,让指向的值也不可以修改
void show_person() const
{
//m_a = 100;//加上const 后不可以修改
//this->m_a = 100;//指针也不可以修改
//this = NULL;//this指针不可以修改真挚的指定向
this->m_b = 100;
}
int m_a;
mutable int m_b;//特殊变量,即使在常量函数中,也可以修改这个值,
//用法,在数据类型前加上,mutable
};
常对象:
-
声明对象前加const称该对象为常对象
-
常对象只能调用常函数
请注意,常函数不可以调用普通成员函数,因为普通成员函数可以修改属性。