目录
1.面向过程与面向对象
C语言是面向过程编程语言,关注的是过程,分析实际问题的步骤,通过对函数的调用解决问题
而C++不同于C语言是一门面向对象语言,关注的是对象,将实际问题按照不同的环节,拆分成多个不同对象,通过对象与对象之间的交互解决问题
打个比方,在C语言中面向过程就相当于到饭点时,选择自己料理,需要对菜进行洗菜,切菜,炒菜等一系列操作,而C++中的面向对象好比为到饭点时,选择点外卖,更注重自身通过外卖app与商家进行的交互
那么C++是如何在C语言基础上实现,接下来就要引入类(class)
2.类(class)
在原先C语言结构体(struct)只能定义变量,但在C++中,结构体内不仅可以定义变量,也可以定义函数
但struct通常并不是C++对类的定义,而是使用class代替struct定义类
2.1.类的定义
class className
{
//类的主体:由成员变量和成员函数组成
};
class为定义类的关键字,className为类的名字,{}中就是类的主体,需要注意定义class结束时{}后的;不可省略
在类主体中的内容称为类的成员:类的变量称为类的属性或成员变量; 类中的函数称为类的方法或者成员函数(成员函数储存在公共代码区,属于类实例化后的共同成员函数)
类有两种定义方式:
- 声明和定义全部放在类体中,其中若成员函数在类中定义,编译器可能会将其当作内联函数处理
- 类声明放在头文件中,成员函数定义在.cpp文件中(在.cpp文件中定义时,我们需要在成员函数前面添加 类名 + 作用域限定符(::) )
3.类的访问限定符及封装
3.1.访问限定符
在面向对象编程语言共有三大特性分别是 封装 , 继承 与 多态
其中C++的封装就是通过访问限定符实现:
用类将对象的属性与方法结合在一块,让对象更加完善,通过访问权限选择性的将其接口提供给外部的用户使用
在前面提到C++比起struct定义类,更经常用class定义,它们之间的其中区别之一就在于默认访问限定权限上的不同
访问限定符一共分为三种:
[访问限定符说明]
- public修饰的成员在类外可以直接被访问
- protected和private修饰的成员在类外不能直接被访问
- 访问权限作用域从 该访问限定符出现位置开始 直至 下一访问限定符出现时为止(如果后面没有访问限定符,作用域就到 } 即类结束)
- class 的默认访问权限为 private,struct 为 public
- 在同一类中protected与private几乎无区别,但在类与类继承中 对基类成员变量的权限上 二者将会出现区别
注意:访问限定符只在编译时有用,当数据映射到内存后,没有任何访问限定符上的区别
3.2.封装
封装:将数据与操作数据的方法相结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行互动
封装本质上是一种管理,让用户更方便使用类.如:使用计算机
在C++语言中实现封装,可以通过类将数据以及操作数据的方法进行相结合,通过访问权限来隐藏对象内部实现细节,控制哪些方法可以在类外部直接被使用
4.类的作用域
类定义了新的作用域,类的所有成员都在类的作用域中.在类外定义成员是,需要使用作用域限定符(::)指明定义成员属于哪个类域
class Person{
public:
void PrintPersonInfo();
private:
string _name = "xiaoming";
string _id = "123456";
int _age = 18;
};
void Person::PrintPersonInfo()
{
cout << _id << ":" << _name << " " << _age << endl;
}
5.类的实例化
用类类型创建对象的过程,称为类的实例化
- 类是对对象进行描述,是一个类似模型概念的定义,限定了类有那些成员,定义出一个类并没有分配实际的内存空间来存储它; 如:一张空的学生信息表就可看作一个类,来表述学生信息分类
- 一个类可以实例化出多个对象,实例化出的对象 占用实际的物理空间,储存类成员变量
6.类对象模型
6.1.类实例化对象的大小
一个类的大小,实际就是该类中”成员变量”之和,当然要注意 内存对齐 注意空类的大小,空类比较特殊,编译器给了空类一个字节来唯一标识这个类的对象。
6.2.内存对齐规则
与结构体类似
结构体内存对齐规则
- 第一个成员在与结构体偏移量为0的地址处
- 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处(注意: 对齐数 = 编译器默认的对齐数 与 该成员大小 的 较小值)
- 结构体总大小为最大对齐数(在 所有变量类型最大者 与 默认对齐参数 中取最小) 的整数倍
- 如果嵌套结构体情况,嵌套结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍.
7.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,1,11);
d2.Init(2022, 1, 12);
d1.Print();
d2.Print();
return 0;
}
在函数体中没有不同对象的区分,当d1调用函数时,调用Init函数时如何知道给哪个对象初始化呢
实际上C++引入this指针解决上述问题,C++编译器给每个“非静态的成员函数“增加了一个 隐藏 的指针参数, 让该指针指向当前对象(函数运行时调用该函数的对象), 在函数体中所有 “成员变量” 的操作, 都是通过该指针去访问. 只不过所有的操作对用户是透明的, 即用户不需要来传递,编译器自动完成
7.1.this指针特性
- this指针的类型:类类型*const,即成员函数中,不能给this指针赋值
- 只能在"成员函数"的内部使用
- this指针本质上是"成员函数"的形参,当对象调用成员函数时,将对象地址作为实参传递给this形参,因此对象中不存储this指针
- this指针是"成员函数"第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传递,不需要用户传递
8.类的默认6个成员函数
类中没有成员定义,简称为空类,实例化对象占有一字节的内存空间
但空类中实际上并非什么都没有,在没有任何成员定义时,编译器会自动生成6个默认成员函数
默认成员函数:用户没有显示实现,编译器会生成的成员函数称为默认成员函数
class Date{}
8.1.构造函数
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有一个合适的初始值,并且在对象这个生命周期内只调用一次
8.1.1.构造函数特性
构造函数是特殊的成员函数,虽然构造函数名称叫构造,但构造函数的主要任务并不是开空间创建对象,而是初始化对象
其有如下特征:
- 函数名与类名相同
- 无返回值
- 对象实例化时编译器自动调用对应构造函数
- 构造函数可以重载
8.1.2.构造函数赋值
class Date{
public:
//1.无参构造函数
Date()
{}
//2.带参构造函数(支持缺省)
Date(int year, int month,int day)
:_year(year),
_month(month),
_day(day)
{}
private:
int _year;
int _month;
int _day;
};
需要注意:
- 如果通过无参构造函数调用对象时,对象后面不用跟括号,否则成函数声明
Date d(); //编译器报错 warning C4930: “Date d3(void)”: 未调用原型函数
- 如果类中没有显示定义构造函数,则C++编译器会自动生成无参的默认构造函数,然而一旦用户显示定义构造函数,编译器不再生成
在不实现构造函数情况下,编译器会生成默认的构造函数,但在Date实例化对象时调用编译器生成的默认构造函数,其成员变量依旧是随机值,看似并没有任何作用
实际,C++把类型分为内置类型(基本类型)和自定义类型,内置类型就是语言提供的数据类型,如:int / char...,自定义类型就是我们使用class/struct/union等自己定义的类型,而当在 类 中 成员变量 含有 自定义类型成员变量 时,编译器生成的默认构造函数 就会对 自定义类型 掉用它的 构造函数
- 但对于C++内置类型不初始化的缺陷,后续C++11又打了补丁,即:内置类型成员变量在类中声明时可以给默认值
- 无参的构造函数和全缺省的构造函数称为默认构造函数,并且默认构造函数只能有一个(无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数)
8.1.3.初始化列表
可以看到在上图带参构造函数中使用了不同于C语言的方式,此类方式称为初始化列表
初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
为何不用上述方式定义带参构造函数,首先 需要分清虽然上述构造函数调用之后,对象中已经有了一个初始值,但是不能将其称为对 对象中成员变量 的 初始化,构造函数体中的语句只能将其称为赋初值,而不能称作初始化。因为初始化只能初始化一次,而构造函数体内可以多次赋值。
因此在我们定义构造函数时,应使用初始化列表初始化(对于自定义成员变量,一定会先使用初始化列表初始化)
[注意]
- 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
- 类中包含(引用成员变量, const成员变量, 自定义类型成员变量(且该类没有默认构造函数时)),必须放在初始化列表位置进行初始化
- 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后顺序无关
8.2.析构函数
析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的,而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作
class Time
{
public:
~Time()//析构函数
{
cout << "~Time()" << endl;
}
private:
int _hour;
int _minute;
int _second;
};
8.2.1特性
析构函数是特殊的成员函数,其特征如下:
- 析构函数名是在类名前加上字符~
- 无参数无返回值类型
- 一个类中只能有一个析构函数,若为显示定义,系统会自动生成默认的析构函数(析构函数不能重载)
- 对象生命周期结束时,C++编译系统自动调用析构函数
- 编译器生成的默认析构函数,对自定义类型调用它的析构函数
- 如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,若有资源申请一定要写,避免资源泄漏
8.3.拷贝构造函数
拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时有编译器自动调用
class Time
{
public:
Time()
{
_hour = 1;
_minute = 1;
_second = 1;
}
Time(const Time& t)
{
_hour = t._hour;
_minute = t._minute;
_second = t._second;
cout << "Time::Time(const Time&)" << endl;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
private:
// 基本类型(内置类型)
int _year = 1970;
int _month = 1;
int _day = 1;
// 自定义类型
Time _t;
};
int main()
{
Date d1;
// 用已经存在的d1拷贝构造d2,此处会调用Date类的拷贝构造函数
// 但Date类并没有显式定义拷贝构造函数,则编译器会给Date类生成一个默认的拷贝构
造函数
Date d2(d1);
return 0;
}
8.3.1.特征
- 拷贝构造函数是构造函数的一个重载形式
- 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无限递归
- 若为显示定义,编译器会生成默认拷贝构造函数,默认拷贝构造函数对象将内存储存按字节序完成拷贝,这种拷贝叫浅拷贝也叫值拷贝
注意:在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝,而自定义类型是调用其拷贝构造函数完成拷贝
通过第3点了解当类中涉及资源申请时,拷贝构造时一定要写的,否则仅仅只是指针赋值,多一个指向申请内存空间的指针,也就是浅拷贝
8.3.2.拷贝构造使用场景
- 使用已存在对象创建新对象
- 函数参数类型为类类型对象
- 函数返回值为类类型对象
所以,为了提高程序效率,一般对象传参时,尽量使用引用类型,返回时根据实际场景,能用引用尽量用引用,避免不必要的拷贝
8.4.赋值运算符重载
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其 返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似
函数名字为:关键字operator后面接需要重载的运算符符号
函数原型:返回值类型 operator操作符 (参数列表)
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
// bool operator==(Date* this, const Date& d2)
// 这里需要注意的是,左操作数是this,指向调用函数的对象
bool operator==(const Date & d2)
{
return _year == d2._year
&& _month == d2._month
&& _day == d2._day;
}
private:
int _year;
int _month;
int _day;
};
注意:
- 不能通过连接其他符号创建新的操作符,如:operator@
- 重载操作符必须有一个类类型参数
- 用于内置类型的运算符,其含义不能改变
- 作为类成员函数重载时,其形参看似比操作数少1,因为成员函数第一个参数为隐藏的this
- .* :: sizeof ?: .,此5种运算符不能重载
8.4.1.赋值运算符重载(operator=)
1.赋值运算符重载格式
- 参数类型: const T&,传递引用可以提高传参效率
- 返回值类型:T&, 返回引用可以提高返回的效率,有返回值的目的是为了支持类型赋值
- 检测是否自己给自己赋值(无意义行为)
- 返回*this : 要符合连续赋值的含义
2.赋值运算符只能重载成类的成员函数不能重载成全局函数
// 赋值运算符重载成全局函数,注意重载成全局函数时没有this指针了,需要给两个参数
Date& operator=(Date& left, const Date& right)
{
if (&left != &right)
{
left._year = right._year;
left._month = right._month;
left._day = right._day;
}
return left;
}
// 编译失败:
// error C2801: “operator =”必须是非静态成员
原因: 赋值运算符如果不显式实现.编译器会生成一个默认的赋值运算符.此时用户再在类外自己实现 一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值运算符重载只能是类的成员函数
3.用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝(使用规则同拷贝构造)
注意:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值
8.5.取地址及const取地址操作符重载
class Date
{
public:
Date* operator&()
{
return this;
}
const Date* operator&()const
{
return this;
}
private:
int _year; // 年
int _month; // 月
int _day; // 日
};
一般而言,这两个运算符不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比如需要获取到指定的内容
9.const成员
将const修饰的"成员变量"称之为const成员变量,const修饰类成员变量,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改
10.static成员
声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;
用 static修饰的成员函数,称之为静态成员函数.静态成员变量一定要在类外进行初始化
10.1.特性
- 静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区
- 静态成员变量必须在类外定义,定义时不添加static关键字,类中只是声明
- 类静态成员即可用类名::静态成员或者对象.静态成员访问
- 静态成员函数没有隐藏的this指针,不能访问任何非静态成员
- 静态成员也是类的成员,受public,protected,private访问限定符的限制
11.友元(friend)
友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以 友元不宜多用。 友元分为:友元函数和友元类
11.1.友元函数
当需要重载operator<< 或 operator>> 时,发现没办法将其重载成成员函数,因为cout / cin的输出/输入流对象和隐含的this指针在抢占第一个参数的位置.this指针默认是第一个参数也就是左操作数了.但是实际使用中cout / cin 需要是第一个形参对象才能在正常使用.所有要将operator<< / >>重载成全局函数,但如此操作,又会导致类外无法访问成员,此时就引入友元函数解决该问题
//错误
class Date
{
public:
Date(int year, int month, int day)
: _year(year)
, _month(month)
, _day(day)
{}
// d1 << cout; -> d1.operator<<(&d1, cout); 不符合常规调用
// 因为成员函数第一个参数一定是隐藏的this,所以d1必须放在<<的左侧
ostream & operator<<(ostream& _cout)
{
_cout << _year << "-" << _month << "-" << _day << endl;
return _cout;
}
private:
int _year;
int _month;
int _day;
};
//正确
class Date
{
friend ostream& operator<<(ostream& _cout, const Date& d);
friend istream& operator>>(istream& _cin, Date& d);
public:
Date(int year = 1900, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
{}
private:
int _year;
int _month;
int _day;
};
ostream& operator<<(ostream& _cout, const Date& d)
{
_cout << d._year << "-" << d._month << "-" << d._day;
return _cout;
}
istream& operator>>(istream& _cin, Date& d)
{
_cin >> d._year;
_cin >> d._month;
_cin >> d._day;
return _cin;
}
int main()
{
Date d;
cin >> d;
cout << d << endl;
return 0;
}
友元函数可以直接访问类的私有成员,它是定义在类外面的普通函数,不属于任何类,但需要在类的内部声明,声明时需要关键字friend关键字
说明:
- 友元函数可访问类的私有和保护成员,但不是类的成员函数
- 友元函数不能用const修饰
- 友元函数可以在类中定义的任何地方声明,不受作用限定符限制
- 一个函数可以时多个类的友元函数
- 友元函数的调用域普通函数的调用原理相同
11.2.友元类
友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员
- 友元关系是单向的,不具有交换性
比如下述Time类和Date类,在Time类中声明Date类为其友元,那么Date类中可以直接访问Time类的私有成员变量,但想在Time类中访问Date类中私有成员变量则不行
- 友元关系不能传递
如果C是B的友元,B是A的友元,但不能说明C是A的友元
- 友元关系不能继承
class Time
{
friend class Date; // 声明日期类为时间类的友元类,则在日期类中就直接访问Time类中的私有成员变量
public:
Time(int hour = 0, int minute = 0, int second = 0)
: _hour(hour)
, _minute(minute)
, _second(second)
{}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
{}
void SetTimeOfDate(int hour, int minute, int second)
{
// 直接访问时间类私有的成员变量
_t._hour = hour;
_t._minute = minute;
_t._second = second;
}
private:
int _year;
int _month;
int _day;
Time _t;
};
12.内部类
如果一个类定义在另一个类的内部,这个内部的类就叫做内部类.内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员.外部类对内部类没有任何优越的访问权限。
注意:内部类就是外部类的友元类,内部类可以通过外部类的对象参数来访问外部类的所有成员,但外部类不是内部类的友元
特性:
- 内部类可以定义在外部类的public,protected,private都是可以的
- 注意内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名
- sizeof(外部类) = 外部类,和内部类没有任何关系
class A
{
private:
static int k;
int h;
public:
class B // B天生就是A的友元
{
public:
void foo(const A& a)
{
cout << k << endl;//OK
cout << a.h << endl;//OK
}
};
};
int A::k = 1;
int main()
{
A::B b;
b.foo(A());
return 0;
}
13.匿名对象
前面我们看到
Date d(); //编译器报错 warning C4930: “Date d3(void)”: 未调用原型函数
不能这么定义对象,因为编译器无法识别这是一个函数声明还是对象定义
但可以这样定义对象
Date ();
这就是一个匿名对象,特点就是不用取名字,声明周期只有当前一行
14.编译器对拷贝对象时的优化
在传参和传返回值过程中,一般编译器会做一些优化,减少对象的拷贝
class A
{
public:
A(int a = 0)
:_a(a)
{
cout << "A(int a)" << endl;
}
A(const A& aa)
:_a(aa._a)
{
cout << "A(const A& aa)" << endl;
}
A& operator=(const A& aa)
{
cout << "A& operator=(const A& aa)" << endl;
if (this != &aa)
{
_a = aa._a;
}
return *this;
}
~A()
{
cout << "~A()" << endl;
}
private:
int _a;
};
void f1(A aa)
{}
A f2()
{
A aa;
return aa;
}
int main()
{
// 传值传参
A aa1;
f1(aa1);
cout << endl;
// 传值返回
f2();
cout << endl;
// 隐式类型,连续构造+拷贝构造->优化为直接构造
f1(1);
// 一个表达式中,连续构造+拷贝构造->优化为一个构造
f1(A(2));
cout << endl;
// 一个表达式中,连续拷贝构造+拷贝构造->优化一个拷贝构造
A aa2 = f2();
cout << endl;
// 一个表达式中,连续拷贝构造+赋值重载->无法优化
aa1 = f2();
cout << endl;
return 0;
}
15.explicit关键字
构造函数不仅可以 构造和初始化 对象,还可以 对于单个参数或者第一个参数无默认值其余均有默认值的构造函数进行类型转换
观察代码可以发现d1 = 2023处编译器报错,倘若我们将explicit关键字去掉:
可以发现此时代码可以通过编译
原因就在于图2代码中将整型(int)2023赋值给了一个自定义类型(Date),此时构造函数发生类型转换先用(int)2023构造一个无名对象, 最后用无名对象给d1对象进行赋值
而关键字explicit就将该行为禁止不允许构造函数的隐式转换
总结:
类 是 对某一类实体(对象)来进行描述的,描述该对象具有哪些属性,哪些方法,描述完成后就形成了一种新的自定义类型,可以用该自定义类型 实例化 具体的对象,相当于将具体的事物抽象成 类