本文将介绍下列内容,并重点对默认成员函数展开介绍:
类的定义类的访问限定符和封装this指针类对象的大小默认成员函数(重点)static成员友员
导言类的定义
类的两种定义方式 类的访问限定符和封装
访问限定符
C++中struct和class的区别是什么? 类的作用域封装 类对象模型
类的实例化类对象的存储方式及大小计算 this指针
this指针的引出this指针的特性this的使用 默认成员函数(重点)
类的6个默认成员函数构造函数
概念特性构造函数赋值初始化列表 析构函数
概念特性 拷贝构造函数
概念特征 赋值运算符重载
运算符重载赋值运算符重载 const成员
const修饰类的成员函数取地址及const取地址操作符重载 static成员
概念特性 友元
友元函数友元类
类的定义class className
{
// 类体:由成员函数和成员变量组成
}; // 一定要注意后面的分号
类的两种定义方式class为定义类的关键字,ClassName为类的名字,{}中为类的主体,注意类定义结束时后面分号。
类中的元素称为类的成员:类中的数据称为类的属性或者成员变量; 类中的函数称为类的方法或者成员函数。
- 声明和定义全部放在类体中,需要注意:成员函数如果在类中定义,编译器可能会将其当成内联函数处理。
//日期
class Date
{
public:
//显示日期
void showInfo()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year; //年
int _month; //月
int day; //日
};
- 声明放在.h文件中,类的定义放在.cpp文件中
//这里是类的头文件date.h,声明放在这里
//日期
class Date
{
public:
//显示日期
void showInfo()
private:
int _year; //年
int _month; //月
int day; //日
};
//这里是类的实现文件date.cpp,定义放在这里
#include "date.h" //需要包含头文件
//显示日期
void Date::showInfo() //指明是属于Date这个类的
{
cout << _year << "-" << _month << "-" << _day << endl;
}
一般情况下,更推荐类的声明和定义分离这种方式。
类的访问限定符和封装 访问限定符C++实现封装的方式:用类将对象的属性与方法结合在一块,让对象更加完善,通过访问权限选择性的将其接口提供给外部的用户使用。
【访问限定符说明】
- public修饰的成员在类外可以直接被访问protected和private修饰的成员在类外不能直接被访问(此处protected和private是类似的)访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止如果后面没有访问限定符,作用域就到 } 类结束class的默认访问权限为private,struct为public(因为struct要兼容C)
C++中struct和class的区别是什么?注意:访问限定符只在编译时有用,当数据映射到内存后,没有任何访问限定符上的区别
解答:C++需要兼容C语言,所以C++中struct可以当成结构体去使用。另外C++中struct还可以用来定义类。和class是定义类是一样的,区别是struct的成员默认访问方式是public,class是的成员默认访问方式是private。
类的作用域类定义了一个新的作用域,类的所有成员都在类的作用域中。在类体外定义成员,需要使用 :: 作用域解析符指明成员属于哪个类域。
class People
{
public:
void PrintfInfo();
private:
char _name[20];
int _age;
}
//这里需要指明PrintfInfo是属于People这个类域
void Pelple::void PrintfInfo()
{
cout << _name << " " << _age << endl;
}
封装
封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。
类对象模型 类的实例化封装本质上是一种管理
我们使用protected/private把成员封装起来。开放一些共有的成员函数对成员合理的访问。所以封装本质是一种管理。
用类类型创建对象的过程,称为类的实例化
- 类只是一个模型一样的东西,限定了类有哪些成员,定义出一个类并没有分配实际的内存空间来存储它。一个类可以实例化出多个对象,实例化出的对象 占用实际的物理空间,存储类成员变量。
存储方式:
只保存成员变量,成员函数存放在公共的代码段
大小计算:
this指针 this指针的引出一个类的大小,实际就是该类中”成员变量”之和,当然也要进行内存对齐,注意空类的大小,空类比较特殊,编译器给了空类一个字节来唯一标识这个类。
class Data
{
public :
void Display ()
{
cout <<_year<< "-" <<_month << "-"<< _day <
对于上述类,有这样的一个问题:
Date类中有SetDate与Display两个成员函数,函数体中没有关于不同对象的区分,那当s1调用SetDate函数时,该函数是如何知道应该设置s1对象,而不是设置s2对象呢?
C++中通过引入this指针解决该问题,即:C++编译器给每个 “非静态的成员函数“ 增加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有成员变量的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成。
this指针的特性
this指针的类型:类类型* const只能在“成员函数”的内部使用this指针本质上其实是一个成员函数的形参,是对象调用成员函数时,将对象地址作为实参传递给this形参。所以对象中不存储this指针。this指针是成员函数第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传递(有些也放在栈中),不需要用户传递
this的使用
调用成员函数时,不能显示传实参给this定义成员函数时,也不能显示声明形参this在成员函数内部,可以显示使用this
当类对象指向空指针时,调用该类的成员函数可能会出现解引用空指针引发崩溃
A* p = nullptr;
p->Show(); //若Show()中有访问成员变量的操作,则会崩溃
默认成员函数(重点)
类的6个默认成员函数
构造函数
概念
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,保证每个数据成员都有 一个合适的初始值,并且在对象的生命周期内只调用一次。
特性
构造函数是特殊的成员函数,需要注意的是,构造函数的虽然名称叫构造,但是需要注意的是构造函数的主要任务并不是开空间创建对象,而是初始化对象。
其特征如下:
函数名与类名相同。
无返回值。
对象实例化时编译器自动调用对应的构造函数。
构造函数可以重载。
如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。
无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个(语法上可以同时存在,如果有对象定义去调用就会报错)。
注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数(不用参数就可以调用的构造函数)。
C++把类型分成内置类型(基本类型)和自定义类型
内置类型就是语法已经定义好的类型:如int/char/指针/内置数组等等
自定义类型就是我们使用class/struct/union自己定义的类型
我们不写编译器默认生成的构造函数,对内置类型不做初始化处理,
对于自定义类型成员函数变量会去调用它的默认构造函数初始化,如果没有就会报错。
class Date
{
public :
// 1.无参构造函数
Date ()
{
_year = 2000;
_month = 12;
_day = 30;
}
// 2.带参构造函数
Date (int year, int month , int day )
{
_year = year ;
_month = month ;
_day = day ;
}
private :
int _year;
int _month;
int _day;
};
void TestDate()
{
Date d1; // 调用无参构造函数
Date d2 (2015, 1, 1); // 调用带参的构造函数
// 注意:如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明
// 以下代码的函数:声明了d3函数,该函数无参,返回一个日期类型的对象
Date d3();
}
构造函数赋值
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
虽然上述构造函数调用之后,对象中已经有了一个初始值,但是不能将其称作为类对象成员的初始化,构造函数体中的语句只能将其称作为赋初值,而不能称作初始化。因为初始化只能初始化一次,而构造函数体内可以多次赋值。
初始化列表
初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式。
class Date
{
public:
Date(int year, int month, int day)
: _year(year)
, _month(month)
, _day(day)
{}
private:
int _year;
int _month;
int _day;
};
【注意】
每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)类中包含以下成员,必须放在初始化列表位置进行初始化:
引用成员变量const成员变量没有默认构造函数的自定义类型成员 尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化。成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关
析构函数
概念
析构函数:与构造函数功能相反,析构函数不是完成对象的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成类的一些资源清理工作。
特性
析构函数是特殊的成员函数。
其特征如下:
析构函数名是在类名前加上字符 ~。无参数无返回值。一个类有且只有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。对象生命周期结束时,C++编译系统系统自动调用析构函数。我们不写默认生成的析构函数对内置类型成员变量不做处理
对自定义类型成员变量,会去调它的析构函数
typedef int DataType;
class SeqList
{
public :
SeqList (int capacity = 10)
{
_pData = (DataType*)malloc(capacity * sizeof(DataType));
assert(_pData);
_size = 0;
_capacity = capacity;
}
~SeqList()
{
if (_pData)
{
free(_pData ); // 释放堆上的空间
_pData = nullptr; // 将指针置为空
_capacity = 0;
_size = 0;
}
}
private :
int* _pData ;
size_t _size;
size_t _capacity;
};
拷贝构造函数
概念
构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。
特征
拷贝构造函数也是特殊的成员函数。
其特征如下:
拷贝构造函数是构造函数的一个重载形式。拷贝构造函数的参数只有一个且必须使用引用传参,使用传值方式会引发无穷递归调用。
传值会生成形参,于是会接着调用拷贝构造,引发无穷递归调用
若未显示定义,系统生成默认的拷贝构造函数。 默认的拷贝构造函数对内置类型按内存存储按字节序完成拷贝,这种拷贝我们叫做浅拷贝,或者值拷贝。对自定义类型成员,会调用他的拷贝构造。
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
Date d2(d1);
return 0;
}
赋值运算符重载
运算符重载
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字为:关键字operator后面接需要重载的运算符符号。函数原型:返回值类型 operator操作符(参数列表)
注意:
不能通过连接其他符号来创建新的操作符:比如operator@重载操作符必须有一个类类型或者枚举类型的操作数用于内置类型的操作符,其含义不能改变,例如:内置的整型+,不能改变其含义.* 、:: 、sizeof 、?: 、. 注意以上5个运算符不能重载。
赋值运算符重载
赋值运算符主要有四点:
参数类型
是否把传入的参数类型声明为常量引用。
如果传入的参数不是引用而是实例,那么从形参到实参会调用一次拷贝构造函数。把参数声明为引用可以避免这样的无谓消耗,提高代码的效率。同时,在赋值运算符内不会改变传入的实例的状态,因此应该为传入的引用参数加上const关键字。
返回值
是否把返回值的类型声明为该类型的引用,并在函数结束前返回实例自身的引用(即*this)。
只有返回一个引用,才可以允许连续赋值。
检测是否自己给自己赋值
是否判断传入的参数和当前的实例(*this)是不是同一个实例。如果是同一个,则不进行赋值操作,直接返回。
如果不事先判断就进行赋值,那么在释放实例自身的内存的时候就会导致严重的问题:当*this和传入的参数是同一个实例时,那么一旦释放了自身的内存,传入的参数的内存也同时被释放了,因此再也找不到需要赋值的内容了。
一个类如果没有显式定义赋值运算符重载,编译器也会生成一个。
内置类型成员,会完成对象按字节序的值拷贝。自定义类型成员变量,会调用它的operator=
题目:如下为类型CMyString的声明,请为该类型添加赋值运算符函数。
class CMyString
{
public:
CMyString(cgar* pData = nullptr);
CMyString(const CMyString& str);
~CMyString(void);
private:
char* my_pData;
}
经典解法:
CMyString& CMyString::operator=(const CMyString& str)
{
if (this == &str) {
return *this;
}
delete[] m_pData;
m_pData = nullptr;
m_pData = new char(strlen(str.m_pData) + 1);
strcpy(m_pData, str.m_pData);
return *this;
}
存在的问题:
在分配内存之前先用delete释放了内存。如果此时内存不足导致new抛出异常(抛出异常时原来的实例不再保持有效的状态),m_pData将是一个空指针,容易导致程序崩溃。
解决方法:
先new分配新内容再同delete释放已有的内容。先创建一个临时实例,再交换临时实例和原来的实例。
CMyString& CMyString::operatpr(const CMyString& str)
{
if (this != &str) {
CMyString strTemp(str);
//pTemp的内容在出了函数作用域后会自动调用析构函数释放
//交换部分可以自己定义一个交换函数
char* pTemp = strTemp.m_pData;
strTemp.m_pData = m_pData;
m_pData = pTemp;
}
return *this;
}
const成员
const修饰类的成员函数
将const修饰的类成员函数称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。
取地址及const取地址操作符重载
取地址运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比如想让别人获取到指定的内容!
static成员
概念
声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用static修饰的成员函数,称之为静态成员函数。静态的成员变量一定要在类外进行初始化
特性
静态成员为所有类对象所共享,不属于某个具体的实例静态成员变量必须在类外定义,定义时不添加static关键字类静态成员即可用类名::静态成员或者对象.静态成员来访问静态成员函数没有隐藏的this指针,不能访问任何非静态成员静态成员和类的普通成员一样,也有public、protected、private3种访问级别,也可以具有返回值
class A
{
public:
A()
{
_scount++;
}
A(const A& t)
{
_scount++;
}
//加上static修饰符,在类外可以直接调用
static int GetCount()
{
return _count;
}
private:
static int _scount;
}
int A::_scount = 0;
void TestA()
{
cout << A::GetCount() << endl;
A a1;
A a2;
A a3(a2);
cout << A::GetCount() << endl;
}
友元
友元分为:友元函数和友元类
友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。
友元函数
友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加friend关键字。
class Date
{
friend ostream& operator<<(ostream& _cout, const Date& d);
public:
Date(int year)
: _year(year)
{}
private:
int _year;
};
ostream& operator<<(ostream& _cout, const Date& d)
{
_cout<
说明:
友元函数可访问类的私有和保护成员,但不是类的成员函数友元函数不能用const修饰友元函数可以在类定义的任何地方声明,不受类访问限定符限制一个函数可以是多个类的友元函数友元函数的调用与普通函数的调用和原理相同
友元类
友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。
友元关系是单向的,不具有交换性。
比如在Time类和Date类,在Time类中声明Date类为其友元类,那么可以在Date类中直接访问Time类的私有成员变量,但想在Time类中访问Date类中私有的成员变量则不行。友元关系不能传递
如果B是A的友元,C是B的友元,则不能说明C时A的友元。
class Date; // 前置声明
class Time
{
friend class Date;
public:
private:
}



