C++面向对象基础
C++就是模拟现实世界的物理存在
类和对象的根本理解
class类型定义对象时是需要耗损系统资源的,最起码内存中得有属于对象的内存空间。
类是设计的产物,通过概念设计,概念来自于对现实世界的实体分析得到。
对象是属性与方法的集合,是类实例化出来的一个可执行体,目的是为了模拟现实世界的物理存在
成员变量是对象的属性,变量的值代表了对象所处的状态
成员函数是对象的方法,对象和外部环境进行交互的接口
构造函数
任何事物都需要开始,开始就需要资源。每个构造函数都有创建对象的能力,不同的构造函数给对象的初始化都不一样,因此构造函数可以重载。
构造函数由系统调用,对象无法自己调用构造函数;但是对象可以自己调用析构函数(自杀)。
构造函数有返回值,返回的是它所构建的对象的地址。理论上析构函数没有返回值,但是在有些系统里析构函数有返回值,返回值是对象死亡的地址空间
构造函数的作用:
1:创建对象
2:给对象初始化
3:类型的转换
- 把变量赋值给对象(构造临时对象)
- 把对象赋值给变量(重载某个变量的强转)
析构函数只有一个,虽然构造函数可以重载,可以让对象生而不同,但是所以对象死亡都是相同的,
类里面的缺省函数(用户不写,系统会自动创建):
- 带缺省的构造函数
- 拷贝构造函数
- 析构函数
- 带缺省的赋值运算符重载
- &&:右值引用(右值引用的构造,右值引用的赋值)
常性函数与构造函数的再次构造:
对象成员的初始化顺序:
系统提供的缺省拷贝构造函数:
目的:把A对象给B对象
浅拷贝:按位拷贝指把一块内存空间存放的内容按位拷贝给另一块内存,一般是指针给指针赋值时进行浅拷贝,此时只是将一个指针存放的地址按位拷贝给另一个指针,并没有重新开辟空间。
赋值运算符的重载:
无论是构造还是拷贝构造目的都是创建对象。“=”运算符的重载是为了赋值,此时对象已经创建好了,因此不能使用初始化列表的方式进行赋值
临时变量可以被赋值吗?
(一)编译链接过程
程序运行是在虚拟地址空间上进行,虚拟地址空间将程序内容映射到内存空间进行执行
磁盘和内存相比,磁盘速度非常慢,但是磁盘空间大,够用。此时计算机就可以运行大于自身内存的程序,不用将所有程序都放入内存中,执行哪一部分就先将该部分放入内存中,执行下一部分时若不在内存中就将下一部分搬进内存,内存放不下时,先将不用的部分换出去(系统将长时间不使用的数据替换到磁盘上一块指定的空间,这块空间就叫虚拟内存),保证执行程序时它的那一部分在内存中。若程序需要使用存放在虚拟内存的数据,虚拟地址空间不能直接到磁盘上取数据,数据要先从虚拟内存导入内存,然后从内存到虚拟地址空间
函数结构:
1.预编译
2.编译
3.汇编
4.链接
(二)函数调用过程
(三)this指针
- this指针(类类型的一个指针)
- 代表对象自身的指针
- 成员函数调用的时候,会在第一个参数默认传入this指针
- 类中使用到成员的地方,成员前面默认加上this的解引用
- 每一个普通成员方法的第一个入口参数都是一个自身类型的this指针
1.也存在没有this指针的成员方法:静态成员方法
2.this指针在编译期时默认传入
3.this指针是从函数调用的地方来的,哪一个对象调用成员方法,该成员方法就会取该对象的地址
(四)构造函数
定义一个class对象的过程叫做实例化,实例化过程会调用构造函数
构造函数不能手动调用。
函数的调用是通过对象进行调用的,而执行构造函数后才会产生对象,说明当前没有对象。
析构函数可以手动调用,但是对象死亡时又会自动调用析构,因此可能会出现重复析构的问题
- 构造函数:
- 构造函数没有返回值
- 如果用户自己没有实现构造函数,系统会自动生成一个默认构造函数,自动生成的构造函数没有参数,参数列表为空,函数体为空,不做任何事情
- 如果自己实现了构造函数,系统就不会生成默认构造函数
- 在对象进行实例化的时候编译器会根据参数自动调用相应的构造函数
- 不能手动调用
//Stu(Stu * this,const char*name,int age,bool sex)
Stu(/* Stu * this ,*/const char*name,int age=20,bool sex=1)
{
cout << "Stu(const char*name,int _age,bool _sex)" << endl;
if (NULL == name)
{
return;
}
//this->_name = new char[strlen(name) + 1];
_name = new char[strlen(name) + 1];
for (unsigned int i = 0;i < strlen(name) + 1;i++)
{
_name[i] = name[i];
}
//this->_age = age;
_age = age;
//this->_sex = sex;
_sex = sex;
}
构造函数注意点:
- 函数参数:对于用户传进来的参数要防止恶意改变,因此最好加上const
- 防止进行浅拷贝:指针对指针赋值是浅拷贝,浅拷贝后函数内部的指针得到了用户传进来的地址,有可能对用户原本不想修改的数据进行修改
- 调用默认构造函数时不需要加括号,加括号后编译期就会处理成函数声明,由于没有进行函数声明,编译器不做任何操作
(五)拷贝构造
Stu stu5 = stu2;//用一个已经存在的对象实例化一个正在生成的对象
//这个过程叫拷贝构造
- 拷贝构造函数:
- 1.一定要传引用:不传引用就会死递归。拷贝构造的时候实参需要给形参传值,如果不是引用,相当于用形参构造实参,会再一次调用拷贝构造,从而进行死递归。
- 2.防止浅拷贝:主要针对的是堆上内存(指针…) 若无堆上内存浅拷贝无所谓。对于堆上内存一旦进行浅拷贝,拷贝构造完成后的对象成员会和初始化的对象的对象成员共用一块内存空间,析构时初始化对象时正常,但是析构拷贝构造后的对象时会错误,因为第二次后
- 3.用一个已经存在的对象实例化一个正在生成的对象会自动调用拷贝构造函数
- 4.如果没有定义,系统会自动生成默认的拷贝构造函数,默认的拷贝构造函数只进行浅拷贝,_name指针指向同一块内存空间,析构的时候会被析构2次,第二次析构时stu2._name是一个野指针,析构一个野指针程序崩了
//Stu(Stu stu)error:
Stu(const Stu &stu)//若不进行引用,实参给形参传值的过程会造成死循环
{
//防止浅拷贝
if (stu._name != NULL)
{
_name = new char[strlen(stu._name) + 1];
for (unsigned int i = 0;i < strlen(stu._name) + 1;i++)
{
_name[i] = stu._name[i];
}
}
else
{
_name = NULL;
}
_age = stu._age;
_sex = stu._sex;
}
(六)赋值运算符重载
Stu stu4;
stu4 = stu2;//赋值
/*
用已经存在的对象给已存在的对象赋值的时候会自动调用赋值运算符的重载
*/
- 赋值运算符的重载
- 用已经存在的对象给已存在的对象赋值的时候会自动调用赋值运算符的重载
- 如果没有自己实现的赋值运算符,系统会自动生成默认的赋值运算符重载
- 默认的赋值运算符重载函数只进行浅拷贝
注意:
1:防止自赋值
2:防止内存泄露:由于是已经存在的对象,所以该对象的成员若有堆上内存,则重新赋值后会重新进行一次深拷贝,原来的堆上内存若不进行释放就会变成野指针,造成内存泄露。
3:防止浅拷贝
返回值是引用的目的就是方便连等
Stu& operator=(const Stu& src)
{
//防止自赋值
if (this == &src)
{
return*this;
}
//防止内存泄露
if (_name != NULL)
{
delete[]_name;
}
//防止浅拷贝
_name = new char[strlen(src._name) + 1];
for (unsigned int i = 0;i < strlen(src._name) + 1;i++)
{
_name[i] = src._name[i];
}
_age = src._age;
_sex = src._sex;
return *this;
}
(七)析构函数
-
析构函数(释放new的堆空间)
对象存在于主函数栈帧上- 栈上的对象后构造的先析构
- 在对象死亡(主函数花括号结束时)的时候会自动调用析构函数
- 如果自己没有实现析构函数,系统会自动生成一个默认的析构函数
- 析构函数没有参数,所以不存在析构函数的重载
- 如果自己实现了析构函数,系统就不会生成默认的析构函数
~Stu()
{
cout << "~Stu()" << endl;
if (NULL != _name)
{
delete[]_name;
this->_name = NULL;
}
}
(八)对象的生存周期
临时对象:对象的生存周期只在当前语句
隐式构造临时对象
显示构造临时对象
自定义类型:
内置类型: