类和对象之对象的构造与销毁
Posted 可乐不解渴
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了类和对象之对象的构造与销毁相关的知识,希望对你有一定的参考价值。
文章目录
一、类
1.1 类的定义
简单地讲,类是一个包含函数的结构体,因此类的定义与结构类型的定义相似,其格式如下:
class 类名:
{
public:
//公有数据成员或者公有成员函数的定义;
protected:
//保护数据成员或者保护成员函数的定义;
private:
//私有数据成员或者私有成员函数的定义;
}
其中:
~关键字class
表明定义的是一个类。
~类名是类的名称,应该是一个合法的标识符。
~public
、protected
、private
为存取控制属性
(访问权限),用来控制对类的成员的存取,如果前面没有标明任何访问权限,则默认访问的权限为private
。
~类的成员有数据成员
和函数成员
两类,类的数据成员和函数成员统称为类的成员,类的数据成员一般用来描述该类对象的静态属性,称为属性
;函数成员用来描述类行为或动态属性,称为方法。函数成员由函数构成,这些作为类成员的函数因此也称为成员函数
。
★注意:
(1) 在C++中,class和struct都可以有成员函数,包括各类构造函数、析构函数、成员函数,并且都可以有public
、private
、protected
修饰符。
(2) class的成员默认是private
权限,struct默认是public
权限。
(3)C语言中没有class。
1.1.1 类的两种定义方式
(1)声明和定义全部放在类体中,需要注意:成员函数如果在类中定义,编译器可能会将其当成内联函数处理
。
(2)声明放在.h文件中,类的定义放在.cpp文件中
一般情况下,我们更期望采用第二种方式。
1.2 类的对象大小的计算
~用类类型创建对象的过程,称为类的实例化。
~定义出一个类并没有分配实际的内存空间来存储它, 一个类可以实例化出多个对象,实例化出的对象
~占用实际的物理空间,存储类成员变量。 那么如何计算 《类的大小》,其实计算类的大小与计算结构体相同。
~1. 第一个成员在与结构体偏移量为0的地址处。
~2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
★注意:对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。
VS中默认的对齐数为8
~3. 结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍。
~4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是
所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
class Person //人类
{
public:
//显示基本信息
void ShowPerson();
private:
char *m_name; //姓名
int m_age; //年龄
char *m_sex; //性别
};
结果如下图所示:
★结论:一个类的大小,实际就是该类中”成员变量”之和,当然也要进行内存对齐,注意空类的大小,空类比较特殊,编译器给了空类一个字节
来唯一标识这个类。
★ 总结:内存对齐是浪费空间,提高效率
1.3 this
指针
- this指针的类型:
类类型* const
。- ★只能在“成员函数”的内部使用。
- this指针本质上其实是一个成员函数的形参,是对象调用成员函数时,将对象地址作为实参传递给
this
形参。所以对象中不存储this指针。this
指针是成员函数第一个隐含的指针形参,一般情况由编译器通过ecx寄存器
自动传递,不需要用户传递。5.
this
指针的本质:是指针常量
,指针的指向是不可以修改的
在一个类的成员函数中,有时希望引用调用它的对象,对此,C++采用隐含的this
指针来实现。this指针是一个系统预定义的特殊指针,指向当前对象,表示当前对象的地址。
例如:
★面试题
~1. this指针存在哪里?
我们知道由于函数的形参和局部变量都是存放在栈中的,this指针是形参,故存放在栈中。但是有一些编译器例如:VS是存放在ecx
中。
~2. this指针可以为空吗?
(1)下面程序能编译通过吗?
(2)下面程序会崩溃吗?在哪里崩溃?
//空指针访问成员函数
class Person
{
public:
void showClassName()
{
cout << "this is Person class" << endl;
}
void shouPersonAge()
{
//由于成员函数接受到的this指针是空指针,对空指针解引用会发生崩溃
cout << "age =" << m_Age << endl;
}
private:
int m_Age;
};
void test()
{
Person* p = nullptr;
p->showClassName();
//p->shouPersonAge(); //error 错误
}
int main()
{
test();
return 0;
}
我们会发现程序崩溃原因是因为访问了空指针问题,如下图结果所示。
我们把shouPersonAge函数修改一下,防止因为nullptr问题而访问。
void shouPersonAge()
{
if (this == nullptr)
{
return;
}
//报错原因是因为传入的指针是为NULL
cout << "age =" << m_Age << endl; //这里m_Age相当于this->m_Age此时this指向的是一个空指针,也就是对象没有实体
// 导致没有确定的对象,无法访问它的值
}
而showClassName函数不会引发崩溃,由于这里并没有对p这个指针解引用,因为showClassName成员函数的地址没有存到对象内,所以这里不会引发空指针访问的崩溃。
★this
用途
- 当形参和成员变量同名时,可以用
this
指针来区分。- 在类的非静态成员函数中返回对象本身,可以使用
return *this;
★注意
(1)
this
指针不是调用对象的名称,而是指向调用对象的指针的名称。
(2)this
的值不能改变,它总是指向当前调用对象。
1.4 构造函数
在定义一个对象的同时,希望能给它的数据成员赋初值-----对象的初始化。C++程序中的初始化由成员函数中的构造函数来完成。其形式如下:
类名();
- 默认构造函数很多人都会以为是我们不写,编译默认生成的那一个,这个理解是不全面的。
- 1、我们不写,编译器默认生成无参构造函数。
- 2、我们自己写的无参构造函数。
- 3、我们写的全缺省构造函数。
- 4、总结一下:默认构造函数就是不需要参数就可以调用的构造函数。
★构造函数的特殊性质
- 构造函数的函数名必须与定义它的类名同名。
- 构造函数没有返回值,如果在构造函数前加
void
是错误的。- 构造函数被声明定义为公有函数
- 构造函数在建立对象时
由系统自动调用。
- 构造函数可以重载。
★构造函数与其他成员函数的区别
~(1)构造函数的命名必须和类名完全相同,而一般成员函数不能和类名相同。
~(2)构造函数的功能主要用于在创建类的对象时定义初始化的状态,它没有返回值,也不能用void修饰.这就保证了它什么也不用返回,而其他成员函数可以有返回值,如果没有返回值,则必须用void予以说明。
~(3)构造函数不能被直接调用,必须在创建对象时由编译器自动调用,一般成员函数在程序执行到它的时候被调用。
~(4)在定义一个类的时候,如果用户没有定义构造函数,编译器会提供一个默认的构造两数,而成员函数不存在这一特点。
1.5 析构函数
在特定对象使用结束时,还经常需要进行一些清理工作,C++中清理工作由成员函数中的析构函数来完成。
析构函数
(destructor
)也被称为拆构函数,是在对象消失之前的瞬间自动调用的函数,其形式如下:
~构造函数名();
析构函数与构造函数的作用几乎相反,相当于“逆构造函数”。析构函数也是类的一个特殊的公有函数成员,它具有以下特点:
- 析构函数没有任何参数,不能被重载,但可以是虚函数,一个类只有一个析构函数。
- 析构雨数没有返回值。
- 析构函数名在类名前加上一个逻辑非运算符“~” ,以与构造函数相区别。
- 析构函数一般由用户自己定义,在对象消失时由系统自动调用,如果用户没有定义析构函数,系统将自动生成个不做任何事的默认析构函数。
★注意:
对象消失时的清理工作并不是由析构函数完成,而是靠用户在析构函数中添加清理语句完成。
1.6 构造和析构的次序
- 构造从类层次的最根处开始,在每一层中,首先调用基类的构造函数,然后调用成员对象的构造函数。析构则严格按照与构造相反的次序执行,该次序是唯一的,否则编译器将无法自动执行析构过程。
- 一个有趣的现象是,成员对象初始化的次序完全不受它们在初始化表中次序的影响,只由成员对象在类中声明的次序决定。这是因为类的声明是唯一的,而类的构造函数可以有多个,因此会有多个不同次序的初始化表。如果成员对象按照初始化表的次序进行构造,这将导致析构函数无法得到唯一的逆序。
1.7 拷贝构造函数
1.7.1 拷贝构造函数概念
在现实生活中,可能存在一个与你一样的自己,我们称其为双胞胎。
C++提供的复制构造函数用于在建立新对象时讲已存在对象的数据成员值复制给新对象,即用一个已存在的对象初始化一个新建立的对象
。拷贝构造函数与类名相同,其形参是本类的对象的引用
。
定义一个拷贝构造函数的一般形式如下:
类名 (const 类名 &对象名);
1.7.2 拷贝构造函数特征
- 若未显示定义,系统生成默认的拷贝构造函数。编译器将以“位拷贝”的方式自动生成缺省的函数。
- 拷贝构造函数的参数只有一个且必须使用引用传参,使用传值方式会引发无穷递归调用。
1.7.3 拷贝构造函数调用时机
- 当用类的一个对象初始化该类的另一个对象时。
- 如果函数的形参是类的对象,在调用函数将对象作为函数实参传递给函数的形参时。
- 如果函数的返回值是类的对象,函数执行完成时将返回值返回。
★注意
- 拷贝构造函数只是在用一个已存在的对象去初始化新建立的对象时调用,在已存在对象间赋值时,拷贝构造函数将不被调用。
- 用一个常量初始化新建立的对象时,调用构造函数,不调用拷贝构造函数。
- 建立对象时,构造函数与拷贝构造函数有且只有一个被调用。
- 当对象作为函数的返回值时需要调用拷贝构造函数,此时C++将从堆中动态建立一个临时对象,将函数返回的对象复制给该临时对象,并把该临时对象的地址存储到寄存器里,从而由该临时对象完成函数返回值的传递。
如果用户定义有参构造函数,则c++不在提供默认的无参构造函数,但是会提供默认拷贝构造函数。
如果用户定义拷贝构造函数,则c++不会再提供其他构造函数。
1.8 赋值函数
当我们用一个类的对象去给该类的另一个对象赋值时,就会用到该类的赋值函数。而默认生成的赋值函数采用的是“位拷贝”而非“值拷贝”的方式来实现,倘若类中含有指针变量,这个函数注定将出错。
其定义如下
类名& operator = (const 类名& obj)
示例:
Person p1(18); //假设这里的Person类内部只有一个年龄属性时
Person p2(20);
p1=p2; // 调用赋值函数,而非拷贝构造函数
示例
说了那么多,不如我们动手写个例子来的强,这里我们以类 String
的设计与实现为例。
String
的结构如下:
class String
{
public:
String(const char* str = nullptr); // 普通构造函数
String(const String& obj); // 拷贝构造函数
~String(void); // 析构函数
String& operator = (const String & obj); // 赋值函数
private:
char* m_data; // 用于保存字符串
};
// String 的普通构造函数
String::String(const char* str)
{
if (str == nullptr)
{
m_data = new char[1];
*m_data = '\\0';
}
else
{
int length = strlen(str);
m_data = new char[length + 1];
strcpy(m_data, str);
}
}
// String 的析构函数
String::~String(void)
{
delete[] m_data;
}
// 拷贝构造函数
String::String(const String& obj)
{
// 允许操作 other 的私有成员 m_data
int length = strlen(obj.m_data);
m_data = new char[length + 1];
strcpy(m_data, obj.m_data);
}
// 赋值函数
String& String::operator = (const String & obj)
{
// (1) 检查自赋值
if (this == &obj) //注意不能写成 *this=obj
{
//你可能会认为多此一举,难道有人会愚蠢到写出 a = a 这
//样的自赋值语句!的确不会。但是间接的自赋值仍有可能出现,
//例如 b = &a; a = *b;
return *this;
}
// (2) 释放原有的内存资源
delete[] m_data;
// (3)分配新的内存资源,并复制内容
int length = strlen(obj.m_data);
m_data = new char[length + 1];
strcpy(m_data, obj.m_data);
// (4)返回本对象的引用
return *this;
}
★注意
对于任意一个类 A,如果不想编写上述函数,
C++编译器将自动为 A 产生四个缺省的函数,如
A(void);
// 缺省的无参数构造函数
A(const A &a);
//缺省的拷贝构造函数
~A(void);
// 缺省的析构函数
A & operate =(const A &a);
// 缺省的赋值函数
1.9 深浅拷贝
- 在默认的拷贝函数中,上面我们讲过,复制的策略是直接将原对象的数据成员值按照“位拷贝”的方式复制给新对象中对应的数据成员。
但倘若类中含有指针变量,那么拷贝构造函数与赋值函数就隐含了错误。 -
- 以类 String 的两个对象 a,b 为例,假设 a.m_data的内容为"hello", b.m_data 的内容为"world"。现将 a 赋给 b,赋值函数的“位拷贝”意味着执行 b.m_data = a.m_data。
这将造成以下三个错误:
- 以类 String 的两个对象 a,b 为例,假设 a.m_data的内容为"hello", b.m_data 的内容为"world"。现将 a 赋给 b,赋值函数的“位拷贝”意味着执行 b.m_data = a.m_data。
一是 b.m_data 原有的内存没被释放,造成内存泄露。
二是b.m_data 和 a.m_data 指向同一块内存,a 或 b任何一方变动都会影响另一方。
三是在对象被析构时,m_data 被释放了两次。
-
- 所以我们必须像上面写的示例一样自己写一个拷贝构造函数和赋值函数。
1.10 初始化列表
C++中构造函数与其他函数不同的是,除了有名字,参数列表和函数体之外,还可以有初始化列表。
初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式。
必须放在初始化列表中进行初始化的成员如下:
- 初始化
const修饰的类成员
;
常量成员,因为常量只能初始化不能赋值,必须放在初始化列表里面,如下面这种情况:
class Test
{
public:
Test(int a, int b)
{
this->m_a = a;
this->m_b = b; //不能在函数体内初始化,必须使用初始化列表初始化
}
private:
int m_a;
const int m_b;
};
这里相当于的const int m_b;
m_b=b;//常量不可以修改
需要将上述代码改为下面这种即可。
class Test
{
public:
Test(int a, int b) :m_b(b)
{
this->m_a = a;
}
private:
int m_a;
const int m_b;
};
- 初始化
引用成员数据
;
引用类型,引用必须在定义的时候初始化,并且不能重新赋值,必须写在初始化列表里。
class Test
{
public:
Test(int a, int b)
{
this->m_a = a;
this->m_b = b; //不能在函数体内初始化,必须使用初始化列表初始化
}
private:
int m_a;
int &m_b;
};
解决方法:
Test(int a, int b):m_b(b)
{
this->m_a = a;
//this->m_b = b; //不能在函数体内初始化,必须使用初始化列表初始化
}
★注意
- 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关。
如下面代码所示:
class A
{
public:
A(int a):_a1(a) ,_a2(_a1)
{}
void Print()
{
cout<<_a1<<" "<<_a2<<endl;
}
private:
int _a2;
int _a1;
}
int main()
{
A aa(1);
aa.Print();
}
这题看起来是用初始化列表a赋给a1,在把a1赋给a2,结果就是a1与a2的值都是1,但其实不然。
因为成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关。所有先初始化a2=a1,此时a1并未初始化,故此时赋值给a2的是随机值,然后再把a赋值给a1。
所有结果是:a1=1,a2为随机值。
END...
以上是关于类和对象之对象的构造与销毁的主要内容,如果未能解决你的问题,请参考以下文章