类和对象进阶
前言
本文适合 对C++类和对象 有初步了解的读者阅读,
关于面向对象的基本知识不赘述。
一、关于访问限定符的几点说明
访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止
class的默认访问权限为private,struct为public(因为struct要兼容C)
访问限定符只在编译时有用,当数据映射到内存后,没有任何访问限定符上的区别
二、类的实例化
用类类型创建对象的过程,称为类的实例化
- 类只是一个模型一样的东西,限定了类有哪些成员,定义出一个类并没有分配实际的内存空间来存储它
- 一个类可以实例化出多个对象,实例化出的对象 占用实际的物理空间,存储类成员变量
三、计算类对象的大小
一个类的大小,实际就是该类中”成员变量”之和,当然也要进行内存对齐。
不清楚内存对齐规则的可移步这里🔗
空类比较特殊,编译器给了空类一个字节来唯一标识这个类。
四、深入理解this指针
C++编译器给每个“非静态的成员函数“增加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象)
this指针本质上其实是一个成员函数的形参,是对象调用成员函数时,将对象地址作为实参传递给this形参。所以对象中不存储this指针。
类的静态成员函数因为没有this指针这个参数,所以类的静态成员函数也就无法调用类的非静态成员变量
this指针的类型:类类型* const
this指针存在哪里?
存放在ECX寄存器中。
this指针可以为空吗?
可以为空,当我们调用函数时,如果不需要通过this指向当前对象对其进行操作时this指针可以为空。如果调用的函数需要指向当前对象,并进行操作,则会发生错误(空指针引用)
五、六大默认成员函数
任何一个类在我们不写的情况下,都会自动生成下面6个默认成员函数
六、const修饰成员函数
将const修饰的类成员函数称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改
const对象不可调用非const成员函数
非const对象可以调用const成员函数
七、细谈成员函数
1.构造函数
如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义,编译器将不再生成
无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。
无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认成员函数
C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语法已经定义好的类型:如int/char…,自定义类型就是我们使用class/struct/union自己定义的类型。对于自定义类型成员,编译器生成的默认构造函数调用自定义类型的默认成员函数,对于内置类型编译器不做处理。但是C++11打了一个补丁,在声明变量时可以这样写:(相当于缺省值)
private:
int _year =2022;
int _month =2;
int _day =14 ;
补充:成员变量的命名风格
class Date
{
public:
Date(int year)
{
_year = year;
}
private:
int _year;
};
// 或者这样。
class Date
{
public:
Date(int year)
{
m_year = year;
}
private:
int m_year;
};
其他方式也可以,一般都是加个前缀或者后缀标识区分
2.析构函数
析构函数不是完成对象的销毁,局部对象销毁工作是由编译器完成的。而
对象在销毁时会自动调用析构函数,完成类的一些资源清理工作。
一个类有且只有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。
对象生命周期结束时,C++编译系统系统自动调用析构函数
编译器生成的默认析构函数,对于自定类型成员,调用自定类型成员的析构函数,内置类型不处理
3.拷贝构造函数
只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用
拷贝构造函数是构造函数的一个重载形式。
拷贝构造函数的参数只有一个且必须使用引用传参,使用传值方式传参会进行参数拷贝,调用拷贝构造函数,造成无穷递归调用。
默认的拷贝构造函数按字节序完成拷贝,这种拷贝我们叫做浅拷贝,或者值拷贝
有时候我们必须自己实现具有深拷贝功能的拷贝构造函数
举例说明:
class String
{
public:
String(const char* str = "jack")
{
_str = (char*)malloc(strlen(str) + 1);
strcpy(_str, str);
}
~String()
{
cout << "~String()" << endl;
free(_str);
}
private:
char* _str;
};
int main()
{
String s1("hello");
String s2(s1);
}
这个程序会运行崩溃,因为程序使用默认生成的拷贝构造函数,s1的_str被赋值给s2的_str,两个指针指向同一内存空间:
调用析构函数时,free(_str)语句导致这块内存空间被释放两次,程序崩溃。
注意:
A b(a);
A b=a; 都是拷贝构造函数来创建对象b
强调:这里b对象是不存在的,是用a 对象来构造和初始化b的!!
4.赋值运算符重载函数
C++为了增强代码的可读性引入了运算符重载
运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名以及参数列表
函数原型:返回值类型 operator操作符(参数列表)
operator后面接需要重载的运算符符号
作为类成员的重载函数时,其形参看起来比操作数数目少1
成员函数有一个默认的形参this,限定为第一个形参,指向调用重载函数的对象。
有些运算符不可重载:
赋值运算符重载:
class Date
{
public :
Date& operator=(const Date& d)
{
if(this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this
}
private:
int _year ;
int _month ;
int _day ;
};
★★★★★
几个细节:
返回值和形参设置为Date& ,减少拷贝构造
返回*this(返回调用赋值运算符重载函数的对象),可以实现连续赋值,如
d1=d2=d3;
检查是否给自己赋值
如果没有显式定义赋值运算符重载,编译器也会生成一个,完成对象按字节序的值拷贝
5.取地址及const取地址重载
这两个成员函数一般不用重新定义 ,编译器默认会生成
class Date
{
public :
Date* operator&()
{
return this ;
}
const Date* operator&()const
{
return this ;
}
private :
int _year ;
int _month ;
int _day ;
};
如果不想让别人获取到地址,可以重载为 return nullptr;
七、对象成员的初始化
初始化列表:
以一个冒号开始,接着是一个以逗号分隔的数据成员列表
例如:
class Date
{
public:
Date(int year, int month, int day)
: _year(year)
, _month(month)
, _day(day)
{}
private:
int _year;
int _month;
int _day;
};
构造函数体中的语句只能将其称作为赋初值,而不能称作初始化
如果没有显示写初始化列表,其实使用了默认初始化列表对对象的各成员初始化,所以尽量都使用初始化列表进行初始化
有些初始化需要在函数体内实现:
类中以下成员,必须放在初始化列表位置进行初始化:
①引用成员变量
②const成员变量
③自定义类型成员(该类没有默认构造函数)
因为引用变量和const变量必须在定义时初始化,所以可以理解为,初始化列表是对象成员变量定义的地方。
成员变量初始化顺序:
成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关
补充:
explicit关键字
构造函数不仅可以构造与初始化对象,对于单个参数的构造函数,还具有类型转换的作用(C++11以后,不是单个参数也可以)
class Date
{
public:
Date(int year)
:_year(year)
{}
private:
int _year;
int _month:
int _day;
};
void TestDate()
{
// 用一个整形变量给日期类型对象赋值
// 实际编译器背后会用2019构造一个无名对象
//最后用无名对象给d1对象进行赋值
d1 = 2019;
}
但这种特性并非总是合乎要求的,有时会导致意外的类型转换,因此,C++新增了关键字explicit,用于关闭这种自动特性
explicit Date(int year)
:_year(year)
{}
被explicit关键字修饰的类构造函数,不能进行自动地隐式类型转换,只能显式地进行类型转换
八、static成员
声明为static的类成员称为类的静态成员
用static修饰的成员变量,称之为静态成员变量;
用static修饰的成员函数,称之为静态成员函数。
特性:
- 静态成员为所有类对象所共享,不属于某个具体的实例
- 静态成员变量必须在类外定义,定义时不添加static关键字
- 类静态成员可用类名::静态成员或者对象.静态成员来访问
- 静态成员函数没有隐藏的this指针,不能访问任何非静态成员
- 静态成员和类的普通成员一样,有public、protected、private3种访问级别
九、友元
1.友元函数
定义在类外部的普通函数
在类的内部声明,声明时需要加friend关键字
友元提供了一合种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装
说明:
友元函数可访问类的私有和保护成员,但不是类的成员函数
友元函数不能用const修饰
友元函数可以在类定义的任何地方声明,不受类访问限定符限制
一个函数可以是多个类的友元函数
2.友元类
友元类的所有成员函数都是另一个类的友元函数,都可以访问另一个类中的非公有成员
友元关系是单向的,不具有交换性
友元关系不能传递,如果B是A的友元,C是B的友元,则不能说明C是A的友元
内部类
概念:如果一个类定义在另一个类的内部,这个内部类就叫做内部类。
这个内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去调用内部类。外部类对内部类没有任何优越的访问权限。
内部类就是外部类的友元类,但是外部类不是内部类的友元。
特性:
- 内部类可以定义在外部类的public、protected、private
- 内部类可以直接访问外部类中的static、枚举成员,不需要外部类的对象/类名。
- sizeof(外部类)=外部类,和内部类没有任何关系