C++编程:进阶阶段—4.2对象
目录
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;
}