目录
4.2 对象特征
4.2.1 构造函数和析构函数
4.2.2 构造函数的分类
4.2.3 拷贝函数调用时机
4.2.4 构造函数调用规则
4.2.5 深拷贝与浅拷贝
4.2.6 初始化列表
4.2.7 类对象作为类成员
4.2.8 静态成员
4.2.9 成员变量和成员函数的存储
4.2.10 this指针
4.2.11 空指针访问成员函数
4.2.12 const修饰成员函数
4.2 对象特征
对象的初始化和清理:C++中,每个对象都有初始设置和对象销毁前的清理数据的设置。
4.2.1 构造函数和析构函数
C++中利用构造函数和析构函数对对象进行初始化和清理。这两个函数会被编译器自动调用,完成对象的初始化和清理。如果程序员不提供构造和析构,编译器会提供构造函数和析构函数,但是是空的。
构造函数:创建对象时为对象的成员属性赋值,构造函数由编译器自动调用,无须手动调用。
语法:类名(){}
1.构造函数,没有返回值也不写void;
2.函数名与类名相同;
3.构造函数可以有参数,因此可以发生重载;
4.程序在调用对象时会自动调用构造,无须手动调用,且只调用一次。
析构函数:对象销毁前系统自动调用,执行一些清理工作。
语法:~类名(){}
1.析构函数,没有返回值也不写void;
2.函数名与类名相同,在函数名前加上符号~;
3.析构函数不可以有函数,因此不可以发生重载;
4.程序在对象销毁前会自动调用析构,无须手动调用,且只调用一次。
代码如下:
#include <iostream>
using namespace std;class Person
{
public://构造函数 初始化对象Person(){cout<<"Person构造函数的调用"<<endl;}//析构函数 销毁/清理对象~Person(){cout<<"Person析构函数的调用"<<endl;}
};void test01()
{Person p;//栈上的数据,该函数执行完后,p这个对象会被释放
}int main()
{//对象的初始化和清理test01();Person p;system("pause");return 0;
}
输出如下:
4.2.2 构造函数的分类
按参数分:有参构造、无参构造;
按类型分:普通构造、拷贝构造。
调用方式:括号法、显示法、隐式转换法。
代码如下:
#include <iostream>
using namespace std;//构造函数发分类及调用
class Person
{
public://无参(默认)构造Person(){cout<<"Person的无参构造函数的调用"<<endl;}//有参构造Person(int a){age=a;cout<<"Person的有参构造函数的调用"<<endl;}//拷贝构造函数(将Person p的属性拷贝过来)Person(const Person &p){age=p.age;cout<<"Person的拷贝构造函数的调用"<<endl;}~Person(){cout<<"Person析构函数的调用"<<endl;}private:int age;
};void test01()
{//调用:括号法cout<<"括号法调用构造函数:"<<endl;Person p1;//默认构造函数调用,不用加括号,编译器会认为Person p1();是一个函数声明。Person p2(21);//有参构造函数调用Person p3(p2);//拷贝构造函数调用//调用:显示法cout<<"显示法调用构造函数:"<<endl;Person p4;Person p5=Person(21);//有参构造Person P6=Person(p5);//拷贝构造Person(21);//表示一个匿名对象,在等式左边的P2就是给他取的名字,匿名对象执行后会立即回收。cout<<"匿名对象清理后执行了这句代码"<<endl;//PS:不要用拷贝构造 初始化匿名对象,编译器会认为Person(p3);是一个对象声明Person p3;//Person(p3);//调用:隐式转换法cout<<"显示法调用构造函数:"<<endl;Person p7=21;//有参构造Person p8=p7;//拷贝构造
}int main()
{test01();return 0;
}
输出如下:
4.2.3 拷贝函数调用时机
- 使用一个已经创建完毕的对象来初始化一个新对象;
- 值传递的方式给函数参数传值;
- 以值方式返回局部对象。
代码如下:
#include <iostream>
using namespace std;//拷贝构造函数调用时机
class Person
{
public://无参(默认)构造Person(){cout<<"Person的无参构造函数的调用"<<endl;}//有参构造Person(int a){age=a;cout<<"Person的有参构造函数的调用"<<endl;}//拷贝构造函数Person(const Person &p){age=p.age;cout<<"Person的拷贝构造函数的调用"<<endl;}~Person(){cout<<"Person析构函数的调用"<<endl;}int age;
};//使用一个已经创建完毕的对象来初始化一个新对象
void test01()
{cout<<"test01函数调用"<<endl;Person p1(21);Person p2(p1);cout<<"p2的年龄为:"<<p2.age<<endl;
}//值传递的方式给函数参数传值
void doWork(Person p)
{}void test02()
{cout<<"test02函数调用"<<endl;Person p;doWork(p);//这里传入的p和dowork中的p不一样
}//以值方式返回局部对象
Person doWork2()
{Person p1;cout<<"p1的地址为:"<<(long long)&p1<<endl;return Person(p1);//直接返回p1则不会调用拷贝函数,因为编译器自动做了优化(可以看到p1和p的地址一样)
}void test03()
{cout<<"test03函数调用"<<endl;Person p=doWork2();cout<<"p的地址为:"<<(long long)&p<<endl;
}int main()
{test01();test02();test03();return 0;
}
输出如下:
4.2.4 构造函数调用规则
默认情况下,C++编译器至少给类添加三个函数;
- 1.默认构造函数(无参,函数体为空)
- 2.默认析构函数(无参,函数体为空)
- 3.默认拷贝构造函数,对属性进行值拷贝
调用规则:
- 如果用户定义了有参构造函数,则编译器不提供默认无参构造,但会提供默认拷贝构造
- 如果用户定义了拷贝构造函数,则编译器不再提供其他构造函数
代码如下:
#include <iostream>
using namespace std;class Person
{
public:// Person()// {// cout<<"person的默认构造函数"<<endl;// }Person(int a){age=a;cout<<"Person的有参构造函数的调用"<<endl;}// Person(const Person &p)// {// age=p.age;// cout<<"Person的拷贝构造函数的调用"<<endl;// }~Person(){cout<<"Person析构函数的调用"<<endl;}int age;
};// void test01()
// {
// Person p;
// p.age=18;// Person p2(p);
// cout<<"p2的年龄为:"<<p2.age<<endl;
// }void test02()
{Person p(28);Person p2(p);cout<<"p2的年龄为:"<<p2.age<<endl;
}int main()
{//test01();test02();return 0;
}
输出如下:用户定义了拷贝构造函数
输出如下:用户没有定义拷贝构造函数
错误示例:用户定义了有参构造,但没有定义无参(默认)构造,则编译器也不会提供默认构造,此时调用默认构造则会报错。
输出如下:用户只定义了有参构造,则编译器依然或提供拷贝构造
4.2.5 深拷贝与浅拷贝
浅拷贝:编译器提供的拷贝函数,简单的赋值拷贝操作;
缺点:容易导致堆区的重复释放,利用深拷贝解决。
深拷贝:在堆区重新申请空间,进行拷贝操作,而不是与被拷贝的指针指向相同的空间。
PS:如果属性有在堆区开辟的,一定要自己定义拷贝构造函数,防止浅拷贝中出现的问题。
代码如下:
#include <iostream>
using namespace std;class Person
{
public:Person(){cout<<"person的默认构造函数"<<endl;}Person(int a,int h){age=a;height=new int(h);cout<<"Person的有参构造函数的调用"<<endl;}//自己实现拷贝构造函数,解决浅拷贝的问题Person(const Person &p){age=p.age;height=p.height;//编译器写的(浅拷贝)height= new int(*p.height);//深拷贝操作,另外开辟空间cout<<"Person的拷贝构造函数的调用"<<endl;}~Person(){//析构的作用,将堆区new的数据手动释放if(height!=NULL)//若指针不为空,则需要释放{delete height;//P2先释放,完了之后P也需要释放,但两个对象的指针操作的是同一个堆区中的地址,造成重复释放的非法操作,因此会报错height=NULL;//防止野指针出现,将指针置空}cout<<"Person析构函数的调用"<<endl;}int age;int * height;
};void test01()
{Person p(28,160);Person p2(p);cout<<"p2的年龄为:"<<p2.age<<" 身高为:"<<*p2.height<<endl;
}int main()
{test01();return 0;
}
输出如下:
4.2.6 初始化列表
作用:C++提供了初始化列表语法,用来初始化属性。
语法:构造函数():属性1(值1),属性2(值2)...{}
代码如下:
#include <iostream>
using namespace std;class Person
{
public://传统初始化操作// Person(int a,int b,int c)// {// A=a;// B=b;// C=c;// }//初始化列表赋初值//Person():A(1),B(2),C(3){}Person(int a,int b,int c):A(a),B(b),C(c){}int A;int B;int C;
};void test01()
{//Person p(10,20,30);//传统赋值Person p(1,2,3);//列表赋值cout<<"A="<<p.A<<endl;cout<<"B="<<p.B<<endl;cout<<"C="<<p.C<<endl;}int main()
{test01();return 0;
}
输出如下:
4.2.7 类对象作为类成员
C++中类的成员可以是另一个类的对象,称为对象成员。
注意对象作为成员时,两种对象的构造和析构函数的顺序。(先构造其他类,再构造本类,先析构本类,再析构其他类)
代码如下:
#include <iostream>
using namespace std;
#include <string>//对象成员
class Phone
{
public://手机品牌string PName;Phone(string pname){cout<<"Phone的构造函数的调用"<<endl;PName=pname;}~Phone(){cout<<"Phone析构函数的调用"<<endl;}
};class Person
{
public://P(pname)相当于Phone P=pname; 隐式转换法Person(string name,string pname):Name(name),P(pname){cout<<"Person的构造函数的调用"<<endl;}~Person(){cout<<"Person析构函数的调用"<<endl;}string Name;Phone P;
};void test01()
{Person p("张三","iPhone18");cout<<p.Name<<"拿着"<<p.P.PName<<endl;
}int main()
{test01();return 0;
}
输出如下:
4.2.8 静态成员
静态成员是指在成员变量和成员函数卡加static关键字,静态成员都有三种访问权限。
静态成员变量:
- 所有对象共享同一份数据
- 在编译阶段分配内存(程序运行前)
- 类内声明,类外初始化
静态成员函数
- 所有对象共享同一个函数
- 静态成员函数只能访问静态成员变量
代码如下:
#include <iostream>
using namespace std;
#include <string>//静态成员
class Person
{
public://静态成员变量static int A;//类内声明int B;//静态成员函数static void func(){A=44;//B=22;//静态成员函数访问非静态成员变量,报错,无法区分是哪个对象的Bcout<<"静态成员函数调用"<<endl;}
};//类外初始化
int Person::A=100;void test01()
{Person p;cout<<p.A<<endl;Person p2;p2.A=200;//所有对象共享同一份数据,因此有两种访问方式:通过对象访问;通过类名访问cout<<p.A<<endl;cout<<Person::A<<endl;
}void test02()
{//两种访问方式:通过对象访问;通过类名访问Person p;p.func();Person::func();cout<<p.A<<endl;
}int main()
{test01();test02();return 0;
}
输出如下:
错误示例:静态成员函数访问非静态成员变量
4.2.9 成员变量和成员函数的存储
类内的成员变量和成员函数分开存储,只有非静态成员变量才属于类的对象上的。
代码如下:
#include <iostream>
using namespace std;
#include <string>//静态成员
class Person1
{};class Person2
{int A;//非静态成员变量
};class Person3
{int A;static int B;//静态成员变量
};int Person3::B=9;class Person4
{int A;static int B;void func(){}//非静态成员函数
};class Person5
{int A;static int B;void func(){}//非静态成员函数static void func2(){};
};void test01()
{Person1 p1;//空对象占用内存为1,为了区分空对象占内存的位置,每个空对象有一个唯一的地址cout<<"size of p1="<<sizeof(p1)<<endl;Person2 p2;//有非静态成员变量,占4字节 属于类的对象上的数据cout<<"size of p2="<<sizeof(p2)<<endl;Person3 p3;//有静态成员变量 不属于类的对象上的数据cout<<"size of p3="<<sizeof(p3)<<endl;Person4 p4;//非静态成员函数 不属于类的对象上的数据cout<<"size of p4="<<sizeof(p4)<<endl;Person5 p5;//静态成员函数 不属于类的对象上的数据cout<<"size of p5="<<sizeof(p5)<<endl;
}int main()
{test01();return 0;
}
输出如下:
4.2.10 this指针
每一个非静态成员函数只会产生一个函数实例,所有同类中的多个对象会公用一块代码。
C++提供this指针来指向被调用的成员函数所属的对象。this指针是隐含每一个非静态成员函数内的一种指针,不需要定义,直接使用即可。
用途:
- 当形参和成员变量同名时,用this指针来区分
- 在类的非静态成员函数中返回对象本身,可使用return *this。
PS:用Person&定义返回值类型,是因为可以一直对同一个空间操作,用Person定义返回值类型表示值返回,会复制一份新的数据(按照本体p2创建了新的数据,而不是返回的p2本体),调用了拷贝构造函数。
代码如下:
#include <iostream>
using namespace std;
#include <string>class Person
{
public:int age;Person(int age){//age=age;//报错//this指针指向被调用的成员函数所属的对象p1this->age=age;}
//用Person&定义返回值类型,是因为可以一直对同一个空间操作,用Person定义返回值类型表示值返回,会复制一份新的数据(按照本体p2创建了新的数据,而不是返回的p2本体),调用了拷贝构造函数Person& PersonAddAge(Person &p){this->age+=p.age;//this指向p2的指针,*p2指向p2本体return *this;}
};//解决名称冲突
void test01()
{Person p1(18);cout<<p1.age<<endl;
}//用*this 返回对象本身
void test02()
{Person p1(31);Person p2(31);p2.PersonAddAge(p1);cout<<p2.age<<endl;p2.PersonAddAge(p1).PersonAddAge(p1);//用this*返回才能链式追加cout<<p2.age<<endl;}int main()
{test01();test02();return 0;
}
输出如下:
错误示例:名称冲突,形参和属性名相同时,不能输出正确结果
4.2.11 空指针访问成员函数
C++中空指针可以调用成员函数,但需要注意有没有用this指针。如果用到this指针,需要加以判断保证代码的健壮性。
代码如下:
#include <iostream>
using namespace std;//空指针调用成员函数
class Person
{
public:void showClassName(){cout<<"this is person class"<<endl;}void showPersonAge(){if(this==NULL){return;}//传入指针为空,报错 在前面加一个空指针的判断cout<<"age="<<this->age<<endl;}int age;
};void test01()
{Person *p=NULL;p->showClassName();p->showPersonAge();
}int main()
{test01();return 0;
}
输出如下:
错误示例:用空指针访问属性,图中age,默认是this->age,而访问时用的空指针,this为空所以不能指向正确的对象的属性。
4.2.12 const修饰成员函数
常函数:
- 成员函数后加const后称为常函数;
- 常函数内不可用修改成员属性;
- 成员属性声明时加关键词mutable后,在常函数中依然可以修改
常对象:
- 声明对象前加const称该对象为常对象;
-
常对象不允许修改指针指向的值;
- 常对象只能调用常函数
代码如下:
#include <iostream>
using namespace std;//常函数
class Person
{
public://this指针的本质是一个指针常量Person * const this 指针的指向是不可修改的//后面加的const相当于const Person * const this,使this指向的值也不可修改void showPerson() const{this->b=99;//this=NULL;//this的指针指向不能修改cout<<"this is person class"<<endl;}Person(){}//不写默认构造函数会报错实例化的常对象没有初始化void func(){}int age;mutable int b;
};void test01()
{Person p;p.showPerson();
}void test02()
{const Person p;//p.age=99;//报错 常对象不允许修改指针指向的值p.b=88;p.showPerson();//p.func();//报错 常对象不能调用非常函数
}int main()
{test01();test02();return 0;
}