C++是基于面向对象的,关注的是对象,将一件事情拆分成不同的对象,靠对象之间的交互完成。
面向对象有三大特性:封装、继承、多态。
一个类型=属性+方法。
C++首先对struct进行升级,在struct的基础上引入了类,1、兼容C中struct的所有用法;2、升级成为了类。
比如以前的结构体里,只有属性(C:属性),方法放在struct外面,要用属性时,还要传指针;cpp里可以把函数也写进去(CPP:属性+方法),方法在struct内部,属性可以直接使用,无需指针。
struct里面的就称为成员变量、成员函数。使用结构体的访问方式。
之后就出现了类Class。
Class
类体中内容称为类的成员:类中的变量称为类的属性或成员变量; 类中的函数称为类的方法或者成员函数。
为实现封装,提出访问限定符:public
(公有)、private
(私有)、protected
(保护)。
class classname
{
//这里面也是一个类域
// 类体:由成员函数和成员变量组成
public:
//...
private:
//...
};// 一定要注意后面的分号
成员变量命名规则的建议:加个前缀_
、m
或者后缀标识区分成员函数的形参。
定义方式
类的两种定义方式:
-
声明和定义全部放在类体中,需注意:成员函数如果在类中定义,编译器可能会将其当成内联函数处理。
-
类声明放在.h文件中,成员函数定义放在.cpp文件中,注意:成员函数名前需要加类名
::
。分离是为了方便阅读。
一般情况下,更期望采用第二种方式。
注意:访问限定符只在编译时有用,当数据映射到内存后,没有任何访问限定符上的区别。
访问限定符说明
1、public
修饰的成员在类外可以直接被访问;
2、private
和protected
修饰的成员在类外不能直接被访问;
3、class
的默认访问权限为private
,struct
的为public
,两者都可以定义访问限定符;
4、访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止,如果后面没有访问限定符,作用域就到 }
即类结束。
问题:C++中struct和class的区别是什么?
解答:C++需要兼容C语言,所以C++中struct可以当成结构体使用。另外C++中struct还可以用来定义类。和class定义类是一样的,区别是struct定义的类默认访问权限是public,class定义的类默认访问权限是private。注意:在继承和模板参数列表位置,struct和class也有区别,待补充。
封装
在类和对象阶段,主要是研究类的封装特性,那什么是封装呢?
封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。
封装本质上是一种管理,让用户更方便使用类。在C++语言中实现封装,可以通过类将数据以及操作数据的方法进行有机结合,通过访问权限来隐藏对象内部实现细节,控制哪些方法可以在类外部直接被使用。
类的作用域
类定义了一个新的作用域,类的所有成员都在类的作用域中。在类体外定义成员时,需要使用 ::
作用域操作符指明成员属于哪个类域。
在声明和定义分离的情况下,就体现了类域的作用。公有成员函数前要加上域作用限定符才行。因为不同类可能有相同的成员函数,就利用作用域来区分函数。
命名空间::名称
类名::成员变量(类内)
类名.公有成员变量(类外)
类名::静态成员变量(类内外)
类名::成员函数(仅类外定义时可用)不可在其他函数中用域作用限定符去定义该成员函数,因为这样写不会带上this指针
类名::静态成员函数(在其他函数中定义)
类名.成员函数(调用时使用)这种写法编译器会自动用是this指针
//后面有“::”的名称一定是类名或命名空间名
//在其他函数中定义成员函数,报错如下:非静态成员引用必须与特定对象相对。意思是若要这样访问,这些成员函数或成员变量得是静态成员。
那为什么命名空间可以访问其中的成员呢?后续补充。
因为类内的成员变量只是定义,而非实例。
类的实例化
用类这个类型创建对象的过程,称为类的实例化。
-
类是对对象进行描述的,是一个模型一样的东西,限定了类有哪些成员,定义出一个类并没有分配实际的内存空间来存储它;
-
一个类可以实例化出多个对象,实例化出的对象 占用实际的物理空间,存储类成员变量;
类对象模型
类的大小计算
类对象的存储方式:只保存成员变量,成员函数存放在公共的代码段。因为成员函数都是一样的,成员变量可能不一样。
故一个类的大小,实际就是该类中”成员变量”之和,当然要注意内存对齐。注意空类的大小,空类比较特殊,编译器给了空类一个字节来唯一标识这个类的对象。
只有成员函数的类或者空类,大小是1,表示占位标识对象存在,不存储有效数据。
this指针
先看一段代码,为什么调用同一个函数,有不一样的结果?
class Date
{
public:
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "-" << _month << "-"<< _day << endl;
}
private:
int _year; // 年
int _month; // 月
int _day; // 日
};
int main()
{
Date d1, d2;
d1.Init(2022, 12, 11);//Init(&d1,2022,12,11); //结果为2022-12-11
d2.Init(2022, 12, 12);//Init(&d1,2022,12,12); //结果为2022-12-12
d1.Print();//Print(&d1);
d2.Print();//Print(&d2);
return 0;
}
Print()函数并没有传参,为什么d1和d2调用Print函数的结果不一样呢?
实际上编译器会对该函数进行一个处理,加上一个类指针(只不过这是隐含参数),这里this
是一个关键字。在调用该函数的时候会变成Print(&d1);
。
void Print(Date* const this)//保护指针
{
cout << this->_year << "-" << this->_month << "-" << this->_day << endl;
}
// const和指针复习
Date* const this//修饰的是this,指针,不可以改指针的指向
const Date* this//修饰的是*this,指针指向的对象,即_year、_month、_day
Date const* this//修饰的是*this,指针指向的对象
C++中通过引入this指针解决该问题,即:C++编译器给每个“非静态的成员函数“增加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有“成员变量”的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成。
所以回到一个问题,为什么不能通过类去访问成员函数而是要通过对象去访问,因为类只是个类型,它没有地址。
this指针是个形参,存在栈帧里。VS下进行了优化,使用了ecx寄存器来传递。
this指针的特性
1、this指针的类型:类类型* const
,即成员函数中,不能给this指针赋值;2、只能在“成员函数”的内部使用;3、this指针本质上是“成员函数”的形参,当对象调用成员函数时,将对象地址作为实参传递给this形参。所以对象中不存储this指针,在调用函数时不用传该对象的地址;4、this指针是“成员函数”第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传递,不需要用户传递。5、我们可以在类里面使用this指针
运算符重载
运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。关键字operator
**注意:类内运算符重载时只能有且仅有一个参数;类内其他函数可以有多个参数。**如果参数过多:方法一:利用全局函数类外实现;方法二:利用友元类内实现;方法三:类内单参数实现。
//方法1
bool operator==(const Date& d1, const Date& d2)
{
return d1._year == d2._year
&& d1._month == d2._month
&& d1._day == d2._day;
}
//方法3
bool operator==(const Date& d2)
{
return this->_year == d2._year
&& this->_month == d2._month
&& this->_day == d2._day;
}
函数名字为:关键字operator后面接需要重载的运算符符号。
函数原型:返回值类型 operator操作符(参数列表)
注意:
1、不能通过连接其他符号来创建新的操作符:比如operator@;
2、重载操作符必须有一个类类型参数;
3、用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不 能改变其含义;
4、作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this
5、.* :: sizeof ?: .
这5个运算符不能重载。
前置++和后置++重载
C++规定:后置++重载时多增加一个int类型的参数,但调用函数时该参数不用传递,编译器自动传递。这个形参其实没用,++都是+1,只是为了区分前置和后置,两个构成函数重载。
前置++:返回+1之后的结果;后置++是先使用后+1,因此需要返回+1之前的旧值。
//前置++,this指向的对象函数结束后不会销毁,故以引用方式返回提高效率
Date& operator++()
{
_day += 1;
return *this;
}
//后置++,要返回+1之前的旧值,故需在实现时需要先将this保存一份给temp,然后给this+1,而temp是临时对象,因此只能以值的方式返回,不能返回引用
Date operator++(int)
{
Date temp(*this);
_day += 1;
return temp;
}
对于自定义类型,要避免使用后置++,所以要养成写前置++的习惯。
流提取/流插入运算符重载
cin是istream类的对象,cout是ostream类的对象。
为什么它俩可以自动识别类型?本质的原因是 函数重载。
<<流提取运算符重载
运算符重载是有要求的,如果是二目操作符,重载时函数的第一个参数必须是左操作数。
//类内重载时(这种写法不能链式调用)
void Date::operator<<(ostream& out)
{
out << _year << "/" << _month << "/" << _day << endl;//err :_year _month _day是不能访问的
}
//其调用方式为
d1 << cout;//这个写法很奇怪
d1.operator<<(cout);
//故流提取、流插入 一般不写成成员函数的形式,因为在类内的成员函数默认第一个参数是类对象指针this,此时Date类对象就是左操作数,不符合习惯和书写方式
所以我们写在类外,但是这又会出现一个新问题,无法访问private限定的成员属性。所以引出友元的概念。
注意:在.h文件中写全局变量和函数,会出现报错“重定义某函数”,是因为这个全局函数都被多Date.cpp和main.cpp文件(编译得到Date.o和main.o)展开,这个全局函数就会在Date.o和main.o的符号表里面,链接的时候就会提示重复,是我们定义的函数自己重复了。解决方法:1、用static修饰变成静态函数,静态函数是有规定,仅在当前文件可见,static修饰可以改变链接属性;2、声明和定义分离。(推荐2)3、在.h文件中,用inline修饰。
提示:不会进符号表的函数还有一类,inline内联函数。它不进符号表的原因是 它在用到的地方直接展开了。
//改进1 可以链式调用
ostream& operator<<(ostream& out, const Date& d)
{
out << d._year << "/" << d._month << "/" << d._day << endl;
return out;
}
友元来了。
友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加friend
关键字。
//改进2 可以在类外访问私有成员
class Date
{
friend ostream& operator<<(ostream& out, const Date& d);
friend istream& operator>>(istream& in, Date& d);
public:
Date(int year = 1900, int month = 1, int day = 1)
{}
private:
int _year;
int _month;
int _day;
};
ostream& operator<<(ostream& out, const Date& d)//cout是一个全局对象,故&
{
out << d._year << "/" << d._month << "/" << d._day << endl;
return out;
}
istream& operator>>(istream& in, Date& d)
{
in >> d._year >> d._month >> d._day;
return in;
}
类的6个默认成员函数
默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。默认成员函数是特殊成员函数,用户不写,编译器会自己生成一个,用户写了,编译器就不会生成。对于有些类,需要我们自己写,对于另外一些类,编译器默认生成的就可以用。
初始化和清理:构造函数、析构函数,拷贝复制:拷贝构造函数、赋值重载函数,取地址重载:普通对象取地址重载、const对象取地址重载。
1构造函数
构造函数是一个特殊的成员函数,无返回值(不用写void),名字与类名相同,创建类类型对象(对象实例化时)时由编译器自动调用,以保证每个数据成员都有一个合适的初始值,并且在对象整个生命周期内只调用一次,可以重载,即可以有多个构造函数,有多种初始化方式。但是构造函数的主要任务并不是开空间创建对象,而是初始化对象。
// 1
Date()
{
_year = 1;
_month = 1;
_day = 1;
} // 无参的构造函数,调用写为Date d1;即可,不能写成Date d1();
// 2 与写法1不能同时存在
Date(int year = 1, int month = 2, int day = 3)
{
_year = year;
_month = month;
_day = day;
} //Date d1; | Date d1(1); | Date d1(1,2); | Date d1(1,2,3);
// 3
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
} // Date d2(2022, 12, 11);
无参的构造函数(写法1)、全缺省的构造函数(写法2)以及不写编译器自动生成的构造函数(写法3)都称为默认构造函数,并且默认构造函数只能有一个。可以理解为不需要传参数的构造函数就是默认构造函数。
注意:如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义构造函数(无参、有参、缺省的都行),编译器将不再生成。但是默认构造函数生成了又没什么用,成员变量被初始化为随机值。
C++把类型分成内置类型(基本类型)和自定义类型,内置类型就是语言提供的数据类型,如:int/char/…/指针;自定义类型就是我们使用class/struct/union等自己定义的类型,
类的默认构造函数,对其内置类型不处理,对其自定义类型会自动调用这个自定义类型的默认构造函数。
注意:C++11 中针对内置类型成员不初始化的缺陷,又打了补丁,即:内置类型成员变量在类中声明时可以给默认值。
class Date
{
private:
// 基本类型(内置类型)
int _year = 1970; // 给了默认值(缺省值),不是初始化
int _month = 1;
int _day = 1;
};
什么时候自己写构造函数?看需求,比如日期类就要自己写,不然年月日(是内置类型)不会被编译器处理。所以我们要自己写,最好写成全缺省构造函数。还有栈stack类也需要自己写。
2析构函数
~Date()
析构函数不是完成对对象本身的销毁,是完成对象中资源(malloc等)的清理工作。
析构函数名是在类名前加上字符 ~
;无参数;无返回值类型;一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载;对象生命周期结束时,C++编译系统系统自动调用析构函数。
如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,比如Date类;有资源申请(malloc、fread等)时,一定要写,否则会造成资源泄漏,比如Stack类。
什么时候自己写析构函数?面向需求,当有资源需要手动释放的时候。该类的构造函数使用了malloc、fread等,自定义类型使用了资源就不用,因为该类的析构函数自动调用了。
3拷贝构造函数
Date(const Date& d)
//Date d2(d1);//调用方式
只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。拷贝构造函数是构造函数的一个重载形式;
拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用。【传值传参的时候,因为形参是实参的一份临时拷贝,就会调用拷贝构造函数,故会引发无穷递归。】
若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,而自定义类型是调用其拷贝构造函数完成拷贝的。所以需要注意:类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请时,则拷贝构造函数是一定要写的,否则就是浅拷贝。比如链表、顺序表、二叉树等都需要写拷贝构造函数。
后定义的先析构,符合栈的性质,后定义的先销毁。