目录
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;//静态成员函数访问非静态成员变量,报错,无法区分是哪个对象的B
cout<<"静态成员函数调用"<<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指针指向被调用的成员函数所属的对象p1
this->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;
}