第零部分:C到Cpp的过渡知识
7个知识点
知识点 0:命名空间
避免模块化工作所造成的函数名,变量名命名冲突问题,c++引入命名空间概念,在命名空间内的所有元素生命周期都只局限于该命名空间中,不会和其他变量或函数造成同名冲突
namespace Zone {
//Zone 为命名空间的名称,相同命名空间会自动合成到同一空间(同名空间中不允许出现重复定义)
//可以定义变量,函数等
int a;
namespace Zone1{
//可以嵌套定义
int b;
}
}
最为推荐的命名空间元素使用方式:Zone ::a
或Zone::Zone1::b
知识点 1:标准输出输入
编写C++程序标准输入输出所需要包含头文件
#include <iostream>
using namespace std;
//标准输入输出格式
输入:cin>>b>>a;
输出:cout<<a<<b<<endl; (endl为换行符)
//支持连续输入输出
其中字符串和非字符串输出是有本质区别的:非字符串指针输出首地址,字符串指针输出字符串内容
int a = 10 ;
char* p = "1234";
int* pa = &a;
std::cout << pa << std::endl; //非字符串指针输出地址
std::cout << p << std::endl; //字符串指针输出内容
知识点 2:缺省参数
没指定实参时,采用默认参数值
- 全缺省参数
void TestFunc(int a = 10, int b = 20, int c = 30)
- 半缺省参数
void TestFunc(int a, int b = 10, int c = 20)
参数接收顺序从左向右,因此半缺省参数必须从右往左依次给出,不能存在间隔;缺省参数不能在函数声明和定义中重复出现,编译器无法确定到底使用那个缺省值 - 占位参数:
void TestFunc(int a = 10, int b = 20, int )
第三个则表示占位
知识点 3:函数重载
同名函数形参列表(参数个数,类型,顺序)必须不同,仅仅返回值不同不能够构成函数重载
int Add(int a = 1, int b = 2);
float Add(float a, float b);
int Add(int a, int b ,int c);
int Add(char a, char b) ;
int Add(int a, char b);
int Add(char a, int b)
//让编译器按照C语言编译规则来进行编译
extern "C" {
int sub(int a, int b);
int mul(int a, int b);
}
为何C++支持函数重载,而C语言不支持函数重载?
- C语言编译,函数名修饰没发生任何改变,因此不论参数,类型发生任何改变,都是同一个函数,不会发生重载
- C++编译,编译器会将函数参数类型等具体信息添加到函数名中以确保它的唯一性
- 换言之,C语言编译后同名函数无法进行区分,而C++时通过函数修饰规则来进行区分,即使同名函数调用时底层函数也不同
C和C++编译的本质
知识点 4:引用
对存在的变量取一个别名,称之引用(不会开辟内存空间,使用同一块内存空间,大小名的区别)
引用的效率要高于正常传参的效率
- 标准化引用
int a = 10;
int& ra=a;//引用 引用必须初始化
// int& ra; 这是错误的;
ra =100;// 赋值 引用定义后不会改变实体的指向,因此会将原有内容全部改为100
- const类型的引用
const int a= 10;
const int& ra =a;//const类型必须用const类型来进行引用 且不能够被修改
int& rb =10; //这是错误的,b为常量不能进行引用
const int& rb =10;
double d =2.0 ;
double& rb =d;//引用类型必须和实体类型同类
double& rb =3.0; //错误的
const double &rb = 3.0; //这一操作是允许的
int c=d;
const int& rd = d;//隐式类型转换,所生成的是一个临时整型变量,因此rd指向临时变量,具备常量特性不能够修改
- 引用作返回值
引用作返回值时,必须注意的是返回变量的生命周期一定要大于函数的生命周期
int& Add2(int a) {//函数结束,a已进行销毁,无法进行引用
return ++a;
}
void Test3() {
int a = 0;
int& d= Add2(a);//无法完成 ,因为a在函数结束时候已经销毁,因此出现访问越界的问题
cout << d << endl;
}
引用和指针的区别:
语法上:引用是一个别名,没有独立空间,和其引用实体共用同一空间
底层实现上:是有空间,因为引用按照指针方式来进行实现
- 引用在定义时必须初始化,而指针没要求
- 引用一个实体后,就不能再引用其他实体,而指针可以随意指向任何一个同类型实体
- 没有NULL引用,但有NULL指针
- sizeof含义不同,引用结果为引用类型大小,而指针始终是地址空间所占字节个数
- 引用自加即引用实体增加1,指针自加则表示指针向后偏移一个类型大小
- 存在多级指针,但没多级引用
- 访问实体方式不同,指针需要显示解引用,引用编辑器会自己处理
- 引用比指针用起来更加的安全
知识点 5:内联函数
编译会将函数指令展开,不产生真的开销,有效提高代码效率,可以代替宏函数的使用
内联函数只是一个建议,编译器会根据程序实际情况进行判断,如代码简单则直接展开,否则会按照函数运行
inline int Add(int a, int b)
{
return a + b;
}
知识点 6:auto自动类型
自动类型推导,相当于类型占位符,不代表任何具体类型存在,具体的类型会在编译时进行推导得到
auto b = 2;
auto f = 3.0;
auto c = 'a';
auto d = 'a' + 'b';
cout << typeid(b).name() << endl;//整型
cout << typeid(f).name() << endl;//浮点型
cout << typeid(c).name() << endl;//字符型
cout << typeid(d).name() << endl;//整型也可能是字符型
- auto定义变量必须初始化,且可以定义多个变量
- 定义多个变量,每一个表达式类型要一致
- 对于长类型变量定义使用更加简单
- 定义引用类型必须
auto& ra =b;
- 函数参数类型和数组不能够使用auto
知识点 7:范围for
对于数据范围确定的数据来说,可以使用范围for来进行(范围不确定一定不能使用范围for)
int array[]={1,2,3,4,5,6};
for(auto e: array)
cout<<e<<" ";
知识点 7++:nullptr
NULL拥有二义性,指针空指和整型0,NULL在编译时会被默认为整型0,而不是我们所谓的空指针
而nullptr是指针空指,建议用nullptr表示指针控制,意义更加的明确
int* p=NULL; 等价于 int* p=0;
nullptr类型: nullptr_t
nullptr可以隐式转换成任意指针类型(内置和自定义)
nullptr: 指针空值, 建议用nullptr表示指针空值,意义明确
第壹部分:动态内存管理
C++极度追求性能,对内存有着过分的执着,这就产生了动态内存管理
1. C/C++中的内存分布
首先我们看一段代码:
int globalVar = 1;
static int staticGlobalVar = 1;
void Test()
{
static int staticVar = 1;
int localVar = 1;
int num1[10] = {1, 2, 3, 4};
char char2[] = "abcd";
char* pChar3 = "abcd";
int* ptr1 = (int*)malloc(sizeof (int)*4);
int* ptr2 = (int*)calloc(4, sizeof(int));
int* ptr3 = (int*)realloc(ptr2, sizeof(int)*4);
free (ptr1);
free (ptr3);
}
通过这段代码,来考验考验我们的C语言内存管理问题:
选择题:
选项: A.栈 B.堆 C.数据段 D.代码段
globalVar在哪里?C staticGlobalVar在哪里?_C
staticVar在哪里?C localVar在哪里?A
num1 在哪里?A
char2在哪里?A *char2在哪里?A
pChar3在哪里?A *pChar3在哪里?D
ptr1在哪里?A *ptr1在哪里?B
- 栈又称为堆栈,非静态局部变量/局部常量(const修饰的)/函数参数/返回值等都存放栈中
- 内存映射段是高效的I/O映射方式,用于装载一个共享动态内存库,可使系统接口创建共享内存,做进程间通信
- 堆用与程序运行时动态内存分配
- 数据段存储全局数据(全局变量/常量)和静态数据
- 代码段放置可重复执行的代码和只读常量
2. C/C++动态内存管理方式
C语言内存管理方式
malloc/calloc/realloc和free
是C语言中常使用到的动态内存管理
1. 普通数组空间申请
void test1()
{
int array[10]; //都为随机值
int array2[10] = { 1, 2, 3 }; //除过前三个,其它都为0
int array3[10] = { 0 }; //所有位置都为0
}
2. malloc和calloc以及realloc的空间管理
void test2()
{
//malloc:只进行空间申请,不进行初始化
int* ptr = (int*)malloc(sizeof(int));
*ptr = 4;
//calloc: 进行空间申请 + 零初始化
int* ptr2 = (int*)calloc(1, sizeof(int));
//realloc: 第一个参数为nullptr/NULL, 功能等价于malloc
int* ptr3 = (int*)realloc(nullptr, sizeof(int));
//调整空间大小:
// 1. 直接原地调整大小
// 2. 重新开空间: 重新申请空间,内容拷贝,释放原有空间
int* ptr4 = (int*)realloc(ptr, sizeof(int) * 4);
char* ptr5 = (char*)realloc(ptr2, sizeof(char));
free(ptr3);
free(ptr4);
free(ptr5);
//传入realloc中的空间后续不需要显式释放,会导致二次释放的问题
/*free(ptr);
free(ptr2);*/
}
C++动态内存管理方式
C语言内存管理方式在C++中可以继续使用,但有些地方不仅无能为力且使用起来较麻烦,因此C++提出适合自己的内存管理方式new和delete
操作符进行动态内存管理
void test1()
{
// 单个类型的空间:new + 类型
// 连续空间:new + 类型[个数]
// 单个类型空间申请 + 初始化: new + 类型(初始值)
// 基本类型用new申请连续空间,不能初始化
int* ptr3 = new int;
int* ptr4 = new int[10];
int* ptr5 = new int(5); //初始化为5
//释放空间
//单个空间: delete 指针
//连续空间: delete[] 指针
//申请和释放的操作匹配使用: malloc free, new delete, new [] delete[]
delete ptr3;
delete ptr5;
delete[] ptr4;
}
class Date{
public:
Date(){
}
}
void test2()
{
//动态创建自定义类型的对象:
//new:动态开空间 + 调用构造函数初始化
//申请单个空间: new 自定义类型(参数列表)
Date* pd = new Date(2020);
Date* pd2 = new Date(2030);
Date* pd4 = new Date; //调用默认构造:无参,全缺省
//申请连续的空间:new 自定义类型[个数], 自动调用默认构造进行初始化,如果没有默认构造,编译器报错
Date* pd3 = new Date[10];
//释放自定义类型的空间
//delete: 调用析构函数清理资源 + 释放空间
delete pd;
delete pd2;
delete pd4;
//连续空间: 调用N次析构 + 释放空间
delete[] pd3;
}
- 在申请自定义类型的空间时:
new
会调用构造函数,而delete
会调用析构函数,而malloc和free
不会
operator new和operator delete函数
operator new和operator delete
是系统提供的全局函数,new
在底层调用operator new
来申请空间,delete
在底层调用operator delete
来释放空间,而operator new和operator delete
实现中可以发现还是通过malloc
来进行申请,通过free
来进行释放
void test()
{
//void* operator new(size_t n): 不是运算符重载函数,而是一个全局函数
// : 使用方式和malloc类似
// : 封装malloc + 异常
//new 10;
//new的执行过程(自定义类型):operator new --> malloc --> 构造函数
char* ptr = (char*) operator new(sizeof(char));
char* ptr2 = (char*)malloc(sizeof(char));
//void operator delete(void* ptr):不是运算符重载函数,而是一个全局函数
// :使用方式和free类似
// :封装free
// delete执行过程(自定义类型): 析构 --> operator delete --> free
operator delete(ptr);
free(ptr2);
free(nullptr);
operator delete(nullptr);
}
3. new和delete实现原理
定位new表达式
void test()
{
//new定位表达式,给予申请的空间进行初始化
Date* pd = (Date*)malloc(sizeof(Date));
//new定位表达式: new (地址) 类型(参数列表)
// :在已经开好的空间上显式调用构造函数
new (pd)Date(2030);
Date* pd2 = (Date*)malloc(sizeof(Date));
new (pd2)Date;
}
常见面试题
- malloc/free和new/delete的区别
malloc/free和new/delete
共同点:
都从堆上申请空间,并需用户手动释放
不同点:
- malloc和free是函数,new和delete是操作符
- malloc申请的空间不会初始化,new可以初始化
- malloc申请空间时需手动计算空间大小并传递,new只需在其后跟上空间类型即可
- malloc返回值为void*, 使用时必须强转,new不需要因new后跟的是空间类型
- malloc申请空间失败时,返回的是NULL,因此使用时必须判空,new不需要,但new需要捕获异常
- 申请自定义类型对象时,malloc/free只会开辟空间,不会调用构造函数与析构函数,而new在申请空间后会调用构造函数完成对象的初始化,delete在释放空间前会调用析构函数完成空间中资源的清理
- 什么是内存泄漏,内存泄漏的危害,如何避免内存泄漏
- 什么是内存泄漏:内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。
- 内存泄漏的危害:长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会导致响应越来越慢,最终卡死。
- 避免内存泄漏:实现防御如智能指针;时候差错如泄漏检测工具。
void MemoryLeaks()
{
// 1.内存申请了忘记释放
int* p1 = (int*)malloc(sizeof(int));
int* p2 = new int;
// 2.异常安全问题
int* p3 = new int[10];
Func(); // 这里Func函数抛异常导致 delete[] p3未执行,p3没被释放.
delete[] p3;
}
第贰部分:类与对象
- C语言是面向过程,更多关注过程,分析出求解问题的具体步骤,通过函数调用来逐步解决这个问题
- C++基于面向对象,首先关注对象,将一件事情拆分成不同对象,靠对象之间的交互来完成
类的基本概念
1. 类的定义
两种定义
1.struct 类名 { }
2. class 类名 {}
,重点介绍后面一种
class Student
{
public: //公有的成员在类外可见
//成员函数
void display()
{}
void setId(int id)
{
_id = id;
}
void setNumber(int number)
{
_number = number;
}
private: //私有的成员在类外不可见
//成员变量, 属性
int _number;
int _id;
protected: // 保护的成员在类外不可见
char name[10];
char gender[10];
char major[10];
};
面试题:C++中的struct和class的区别是什么呢?
C++需要兼容C语言,所以C++中的struct可以当成结构体去进行使用,也可以用来定义类;它和class定义类是一样的,区别在于struct的成员默认访问方式是public,class的成员默认访问方式是private
类的访问限定符及封装
上面看到的public和protected,private都是类的访问限定符
对于访问限定符的说明:
- public修饰的成员在类外可以直接被访问
- protected和private所修饰的成员在类外不能直接被访问(protected和private是类似的)
- 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现为止
- class的默认访问权限为private,struct的默认访问权限为public
封装
用类将成员和函数将合在一起,使得对象更加的完善,通过访问权限选择性的将接口提供给外部用户来进行使用,实质上是一种管理
2. 类的作用域和实例化
在类体外定义成员,需使用::
作用域解析符来指明成员属于那个类域
class B
{
private:
int _a;
void fun() {
cout << "class B fun()" << endl;
}
void fun2();
};
void B::fun2() {//类域之中的fun2的定义
cout << "B::fun2()" << endl;
}
类的实例化:
- 创建一个类变量的过程称为类的实例化
- 类只是一张设计图,通过类创建出的变量才是将其变成一个真正实物
- 未实例化前的类不占据空间大小
类大小也遵循内存对齐规则:在类中嵌套类时,如果此类没有创建变量不计算其内存大小
class G
{
char _c; //1
double _d; // 16
int _a; // 20
char _c2; //21
//24
//嵌套类本身遵循内存对齐的原则,计算大小: H: 24
class H
{
double _d; //8
char _c; //9
int _a; //16
char _c1; //17
//24
};
H _h;//未创建变量时,不计算内存大小
};
3. this指针
this指针:
- this指针类型为 类类型
- this指针只存在于成员函数之中,始终指向当前所调用的这个函数,是不能够改变的,是一个形参,不能够算作成员对象
- this始终作为成员函数第一个形参,编译器会自动传递,不需要显式定义此函数
- this不是类的成员,只是一个函数形参,一般存在于栈中,不会做优化
this解引用:
- this未进行解引用,可能是因为内部没有使用this指针所指地址的变量,因此当作一个参数进行传参,不会发生未定义行为
- 如果this所指向的变量需要被改变或使用,这个时候this是一个指针被解引用,若此时this为空会导致异常
类的6个成员函数
一个类中什么成员都没有称为空类:空类并不是什么都没有,任何一个类在什么都不写的情况下,会自动生成6个默认成员函数
1. 构造函数
构造函数是特殊的成员函数,需注意的是构造函数虽名为构造,但其主要任务并不是开空间创建对象,而是初始化对象
- 函数名与类名相同,无返回值,对象实例化时编译器会自动调用所对应的构造函数,构造函数可以重载
- 如果类中存在自定义成员,则构造函数会自动调用自定义成员的默认构造完成初始化,如果自定义成员没有默认构造则会产生编译错误
- 默认构造只能够存在一个,在声明一个函数的时候,是不会调用无参构造的
编译器默认生成的构造函数
Date()
{
}
显示定义的无参构造
Date()
{
_year = y;
_month = m;
_day = d;
}
全缺省的构造函数
Date(int y = 2020, int m = 5, int d = 20)
{
_year = y;
_month = m;
_day = d;
}
重载构造函数
Date(float f)
{
}
explicit关键字
构造函数对于单个参数的构造函数还具有类型转换的作用(调用构造创建一个匿名对象,通过匿名对象来给所需要创建的对象进行赋值或拷贝构造),而为了避免这种隐式转换,可以使用explicit
关键字来修饰构造函数,将会禁止单参构造函数的隐式转换。
构造函数初始化列表
class Time {
public:
Time(int a = 1)
:_a(a)
{
cout << "Time(int)" << endl;
}
private: //这里是成员变量声明的地方,而引用和const类型变量,这两者定义时必须初始化
int _a;
};
- 类中必须放在初始化列表中的有引用成员变量,const成员变量,自定义类型成员(没有默认构造函数),其他成员可以不进行显示初始化
- 每个成员变量在初始化列表中只能出现一次,因为初始化列表是对象成员变量定义的地方
- 成员变量在初始化列表中初始化的顺序,必须和声明顺序一致,与其在初始化列表中的顺序无关(最好保持初始化列表和声明顺序一致)
2. 析构函数
因为类的一些资源并不在类中,因此在对象生命周期结束时候需对资源进行清理和释放(时清理资源不是销毁对象),则自动调用析构函数完成资源清理的工作
- 析构函数名:在类名前加上取反符号
~
,没有参数也没有返回值 - 一个类有且只有一个析构函数,若未进行显式定义,系统会自动生成默认的析构函数
- 在对象生命周期结束时,编译系统会自动调用析构函数
- 如果没有资源需要清理,可以不用显式写析构函数,直接使用编译器默认生成的析构函数即可
class A {
public:
~A()
{
cout << "~A()" << endl;
}
int _a;
};
- 全局对象先于局部对象进行构造
- 静态对象先于普通对象进行构造
3. 拷贝构造函数
拷贝构造是构造函数的一个重载形式,它用一个已经存在的对象去创建一个新的对象,创建的新对象和当前所存在的对象内容完全相同
class Date {
public:
//构造函数
Date(int y = 1, int m = 1, int d = 1) {
_year = y;
_month = m;
_day = d;
}
//拷贝构造函数
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year;
int _month;
int _day;
};
void Test() {
Date d;
Date d2(2020, 4, 1);
Date& rd = d;
Date copy1(d);
Date copy2(Date(2020, 5, 20));//优化,直接调用构造函数创建copy2
//不优化:调用构造创建匿名对象,+ 拷贝构造
}
若没有显式定义拷贝构造函数,系统会默认生成拷贝构造函数,但只会按照内存存储的字节序完成浅拷贝,拷贝对象模型中的内容,不会拷贝资源,如果需要拷贝资源一定要显式定义拷贝构造函数
拷贝构造函数的参数只有一个(一般用const修饰),必须使用引用传参,如果使用传值方式的话会引发无穷次的递归调用。
4. 运算符重载
运算符重载是具有特殊函数名的函数,具备其返回值的类型,函数名,参数列表,其返回值类型和参数列表与普通的函数类似,其作用旨在增强代码的可读性,定义和使用与普通函数一致
class Date {
public:
bool IsEqual(const Date& d) {
return _year == d._year
&& _month == d._month
&& _day == d._day;
}
bool operator==(const Date& d) { //底层接口 bool operator==(Date* const this, const Date& d)
return _year == d._year
&& _month == d._month
&& _day == d._day;
}
private:
int _year;
int _month;
int _day;
};
//输出运算符重载函数
ostream& operator<<(ostream& _cout, const Date& date) {
_cout << date._year << " " << date._month << " " << date._day << endl;
return _cout;
}
- 重载操作符必须有一个类类型或枚举类型的操作数,不能通过连接其他符号来创建新的操作符
- 作为类成员的重载函数时,成员函数的第一个操作符默认为形参this
.* ,::,sizeof,?:, .
以上5个运算符不能重载,笔试选择题中较为热门
赋值运算符重载:
class Date
{
public:
Date(int y = 1, int m = 1, int d = 1)
{
_year = y;
_month = m;
_day = d;
}
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
cout << "Date(const Date& d)" << endl;
}
//优化 ,避免自己给自己赋值
Date& operator=(const Date& d2)
{
//判断是否给自己赋值
if (this != &d2)
{
_year = d2._year;
_month = d2._month;
_day = d2._day;
}
cout << "operator=(const Date& d2)" << endl;
//返回当前调用此函数的对象本身
return *this;
}
//private:
int _year;
int _month;
int _day;
};
void Test() {
Date d(2020, 5, 22);
Date d2(2019, 1, 1);
//如果对象都存在,调用赋值运算符重载函数,如果左边对象不存在,则调用拷贝构造
d2 = d;
d2.operator==(d);// 同上等价
d2 = d2;
Date d3(2018, 10, 1);
//连续赋值:从右向左赋值
d = d2 = decltype;
d.operator=(d2.operator=(d3));//同上面等价
Date d4 = d3;//因为d4不存在,则调用拷贝构造,用d3创建d4对象
}
- 赋值运算符重载函数
d=d2
,修改已经存在的对象内容,不是去创建新的对象 - 如果当前类中有资源存在,必须显式定义赋值运算符重载函数完成深拷贝,否则会采用编译器默认生成的字节拷贝,只能够浅拷贝
- 如所需赋值的对象存在,则直接调用赋值运算符重载函数,如所需赋值对象不存在,则直接调用拷贝构造
5. const成员函数
将const修饰的类成员函数称之为const函数,它实际修饰的是该成员函数的隐式this指针,表明在该成员函数中不能够对类的任何成员进行修改。
const函数和非const函数
void printD() // 等价于 printD(Date* const this)
{
cout << _year << " " << _month << " " << _day << endl;
//可以修改内容
this->_year = 100;
//可以调用const成员函数
fun();
}
void printD() const //等价于 printD(const Date* const this)
{
cout << _year << " " << _month << " " << _day << endl;
//不能修改内容 this->_year = 100;这是错误的
//不能调用非const成员函数,读写的权限不能被放大 fun()
//不能进行自加操作 ++*this;
}
void fun()const
{
}
6. 取地址
class Date
{
public :
Date* operator&()
{
return this ;
}
const Date* operator&()const
{
return this ;
}
private :
int _year ; // 年
int _month ; // 月
int _day ; // 日
};
这两运算符一般不需要重载使用编译器默认生成的即可,如果想要让别人获取到指定的内容的话,才需要进行重载
六大成员函数总结对比
类的三个亲戚
1.静态成员
//int cnt = 0; //定义全局变量的话,安全性较低,容易篡改
class Date
{
public:
Date(int year = 2020, int month = 12, int day = 20)
:_year(year)
, _month(month)
, _day(day)
{
++cnt;
cout << "Date(int ,int ,int)" << endl;
}
Date(const Date& d)
:_year(d._year)
, _month(d._month)
, _day(d._day)
{
++cnt;
cout << "Date(const Date&)" << endl;
}
//静态成员函数:函数内部没有this指针
static int getCount()
{
}
void Display()
{
cout << _year << "-" << _month << "-" << _day << endl;
getCount();
cout << cnt << endl;
}
private:
int _year = 1;
int _month = 1;
int _day = 1;
public:
static int cnt;
};
//静态成员必须在类外初始化
int Date::cnt = 0;
Date fun(Date d) //拷贝构造
{
cout << &d.cnt << endl;
return d;
}
void Test()
{
Date d;//构造
Date d2 = fun(d);//拷贝函数 fun:进行优化,只有两次拷贝构造,传参创建d2
//静态成员变量/静态成员函数访问方式:
// 1. 对象访问
cout << d.getCount() << endl;
cout << d2.getCount() << endl;
cout << &d.cnt << endl;
cout << &d2.cnt << endl;
// 2. 类名 + 作用域限定符
cout << &Date::cnt << endl;
cout << Date::cnt << endl;
cout << Date::getCount() << endl;
//普通成员只能通过对象访问,不能通过类名访问
d.Display();
//Date::Display(); //不支持
}
//对于C++11的初始化方式,相对于给一个缺省值
private:
int _year = 1;
int _month = 1;
int _day = 1;
它将是初始化时候的最后一个候选,先优先前面的进行选择
如果有缺省的构造函数,那么就先优先使用缺省构造函数的数据
2. 友元函数
class Date
{
public:
friend ostream& operator<<(ostream& outputS, Date& d);
friend istream& operator >> (istream& inputS, Date& d);
friend class B;
}
class B
{
public:
//disPlay, fun, fun1都为Date类的的友元函数
void disPlay(const Date& d)
{
cout << d._year << d._month << d._day << endl;
}
void fun(const Date& d)
{
cout << d._year << d._month << d._day << endl;
}
void fun1(const Date& d)
{
cout << d._year << d._month << d._day << endl;
}
};
输出流和输入流
ostream& operator<<(ostream& outputS, Date& d)
{
outputS << d._year << "-" << d._month << "-" << d._day << endl;
return outputS;
}
istream& operator >> (istream& inputS, Date& d)
{
inputS >> d._year >> d._month >> d._day;
return inputS;
}
3. 内部类
enum Color
{
BLACK,
WHITE
};
class C
{
public:
class D
{
public:
void fun(const C& c)
{
//可以通过外部类对象访问外部类的私有成员
cout << c._color << endl;
cout << c._c << endl;
cout << c._sc << endl;
cout << C::_sc << endl;
//可以直接访问外部类的static成员
cout << _sc << endl;
}
private:
int _d;
};
private:
int _c;
static int _sc;
Color _color;
//内部类可以在类的任何地方定义
class E
{
private:
int _e;
};
};
第叁部分:函数/类模板及分离与编译
函数重载可以实现一个通用的函数,但复用率不高,只要有新的类型出现就需增加新的函数,代码的可维护性较低,一个错即所有错,因此引入模板来帮助我们解决这样的问题
1. 泛型编程
编写与类型无关的通用代码,是代码复用的一种手段,而模板则是泛型编程的基础。泛型编程并没有减少实际代码量,但会把重复代码交给机器进行生产,减少开发工作提高效率
template < typename / class 泛型参数1, typename / class泛型参数2,.....>
{
函数定义
}
2. 函数模板及特化
函数模板不是函数,而是将人力所做的事情交给编译器
- 普通函数与模板共存时:先行匹配普通函数(吃现成的,不考虑未做好的饭),若指定实例化,则进行函数模板实例化
- 函数模板特化:对于函数模板不能够处理的特殊类型,一般会定义一个此类型的普通函数
3. 类模板及特化
template <class T1, class T2, class T3>
class Date
{
public:
Date(T1 year, T2 month, T3 day)
:_year(year)
, _month(month)
, _day(day)
{}
void Display();
/*{
cout << _year << "-" << _month << "-" << _day << endl;
}*/
private:
T1 _year;
T2 _month;
T3 _day;
};
template <class T1, class T2, class T3>
void Date<T1, T2, T3>::Display()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
- 如果在类外定义类模板的成员函数,需要加上泛型的声明,作用域为“类名<泛型参数>”
- 类模板不能进行隐式实例化,需要在类模板名字后跟 <>
4. 模板分离编译
一个程序由若干个源文件共同实现,每个源文件单独编译生成目标文件,最后将所有的目标文件链接起来形成单一可执行文件的过程称之为分离编译模式
如需要对上方所出现的问题进行解决,有两种方法:
- 将声明和定义放到一个文件“xxx.h”里面,推荐使用这种
- 模板定义的时候使用显式实例化,但是不实用
总结
【优点】
- 模板复用了代码,节省资源,更快的迭代开发,C++的标准模板库(STL)因此而产生
- 增强了代码的灵活性
【缺陷】 - 模板会导致代码膨胀问题,也会导致编译时间变长
- 出现模板编译错误时,错误信息非常凌乱,不易定位错误
第肆部分:继承与多态
继承
继承是对类层次的复用,它允许在保持原有类的特性基础上进行扩展,增加功能,产生新的派生类(子类),原有的类称之为基类(父类)
1. 继承的访问权限
继承基类成员后访问方式的变化:
- 友元关系不能继承,也就是说父母的朋友不一定是我们的朋友,是不能够相互访问的。
- 基类定义了static静态成员,则整个继承体系里面只有一个这样的成员,无论派生多少个子类,都只会存在这有一个。
- 优先使用对象组合,而不是类继承,类继承一定程度破坏了基类的封装,也被人们称之为白箱复用,而对象组合则是黑箱复用,组合类之间没有较强的依赖性,耦合度低,代码的维护性较好
2. 继承的切片操作
把派生类中父类那部分切割出来进行赋值
3. 同名隐藏及默认成员函数
同名隐藏:
- 父类和子类中有同名的成员,子类只能够直接看到自己的成员
- 如果需要访问父类同名的成员,则需要加上父类的作用域
- 不同的作用域下,含有同名成员的话,当前作用域下的成员就会隐藏其他作用域下的同名成员,这不仅仅是继承体系独有的
成员变量隐藏:成员变量的名称相同
- 函数隐藏:只要函数名相同,就会构成函数隐藏,和参数无关–>这种情况一般是发生在不同作用域的父类和子类中(因此我们在继承体系中最好不要定义同名成员)
- 函数重载:在同一个作用域中,函数名相同,但参数不同的情况。
默认成员函数
- 派生类对象初始化先调用基类构造,再调派生类构造
- 派生类对象析构清理先调用派生类析构,再调用基类的析构
4. 菱形继承
- 单继承:一个子类只有一个父类
- 多继承:一个子类有两个或以上直接父类
- 菱形继承:多继承的一种特殊情况
菱形继承存在着冗余性和二义性,为了更好解决菱形继承的二义性和数据冗余问题,可以使用虚拟继承的方式来进行解决(虚拟继承不能够在其他地方使用)
virtual
class A
{
public:
int _a;
};
class B :virtual public A
{
public:
int _b;
};
class C :virtual public A
{
public:
int _c;
};
class D : public B, public C
{
public:
int _d;
};
多态
不同对象执行同一种行为时所产生的不同状态,被称为多态(比如抢红包,同一点击动作,出现的金额不同
- 非多态的话,要看它的类型
- 多态,只需看实际指向的实体
1. 多态实现条件
- 继承
- 父类定义虚函数,子类重写父类虚函数
- 调用虚函数的类型必须为指针/引用,一般为父类指针/引用
三者条件必须同时满足缺一不可
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl;}
};
重写
- 重写: 派生类中一个跟基类完全相同的虚函数(返回值类型,函数名,参数列表完全相同)当然其中也有列外存在,就是协变函数重写和析构函数的重写
- 协变: 返回值类型可以不同,但返回值类型必须是父子关系的指针/引用
- 析构函数:函数名不相同,看似违背重写规则,但编译后的析构函数却是同一个,因此无论子类析构函数是否加virtual关键字,都与父类析构函数构成重写
虚函数重写
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
virtual ~Person() {cout << "~Person()" << endl;}
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << "买票-半价" << endl; }
virtual ~Student() { cout << "~Student()" << endl; }
}
协变函数重写
virtual B& BuyTicket() {
cout << "买票-半价" << endl;
return B();
}
- 派生类的虚函数即使不加
virtual
关键字,也可以构成重写,因为继承后基类虚函数被继承下来,不建议这样使用 - 父类函数加了virtual声明,则子类接口完全一致的函数,即使不加virtual声明,也具有虚函数属性,建议对所有虚函数都加上virtual
2. 检测重写抽象类
C++给我们提供了override和final
两个关键字,可以帮助我们检测是否重写
- final:修饰虚函数,表示该虚函数不能被继承
class Car
{
public:
virtual void Drive() final {}//使用final定义的函数不能被重写,体现实现继承
};
class Benz :public Car
{
public:
virtual void Drive() {cout << "Benz-舒适" << endl;}
};
- override:检查派生类虚函数是否重写了基类某个虚函数,强制子类重写父类的某一个虚函数,如果没有则重写编译报错
class Car{
public:
virtual void Drive(){}
};
class Benz :public Car {
public:
virtual void Drive() override {cout << "Benz-舒适" << endl;}//体现接口继承
};
重载,重写,隐藏的对比:
3. 抽象类(纯虚函数)
在虚函数的后面写上= 0,则整个函数为纯虚函数(纯虚函数没有函数体),包含纯虚函数的类也被称之为抽象类,不能实例化对象
class Car
{
public:
virtual void Drive() = 0;
};
3. 多态原理
class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
};
sizeof(Base)=4;
在类中,存放着一个_vfptr
指针放在对象前面,将这个指针叫做虚函数表指针,一个含有虚函数的类中至少有一个虚函数表指针,因为虚函数的地址要被放到虚函数表(虚表)中
class Base
{
public:
virtual void Func1()
{
cout << "Base::Func1()" << endl;
}
virtual void Func2()
{
cout << "Base::Func2()" << endl;
}
void Func3()
{
cout << "Base::Func3()" << endl;
}
private:
int _b = 1;
};
class Derive : public Base
{
public:
//虚表:存放函数指针的数组
//
virtual void Func1()
{
cout << "Derive::Func1()" << endl;
}
private:
int _d = 2;
};
void test()
{
Base b;
Derive d;
Base& rb = b;
Base& rd = d;
rb.Func1();
rd.Func1();
rd.Func2();
d.Func1();
}
4. 单继承和多继承的虚函数表(重要)
- 单继承状态下的虚表
class Base
{
public:
virtual void Func1()
{
cout << "Base::Func1()" << endl;
}
virtual void Func2()
{
cout << "Base::Func2()" << endl;
}
void Func3()
{
cout << "Base::Func3()" << endl;
}
private:
int _b = 1;
};
class Derive : public Base
{
public:
//虚表:存放函数指针的数组
//
virtual void Func1()
{
cout << "Derive::Func1()" << endl;
}
private:
int _d = 2;
};
2. 多继承的虚函数表
class Base1 {
public:
virtual void func1() { cout << "Base1::func1" << endl; }
virtual void func2() { cout << "Base1::func2" << endl; }
private:
int b1;
};
class Base2 {
public:
virtual void func1() { cout << "Base2::func1" << endl; }
virtual void func2() { cout << "Base2::func2" << endl; }
private:
int b2;
};
class Derive : public Base1, public Base2 {
public:
virtual void func1() { cout << "Derive::func1" << endl; }
virtual void func3() { cout << "Derive::func3" << endl; }
private:
int d1;
};
//定义 函数指针: void 函数()
typedef void(*vfPtr)();
void doVF(vfPtr* vftable)
{
cout << "虚表地址:" << vftable << endl;
//nullptr: 结束
for (int i = 0; vftable[i] != nullptr; ++i)
{
//获取当前虚表位置的函数指针
vfPtr func = vftable[i];
//执行指向的函数
func();
}
}
void test()
{
Base1 b;
Base2 b2;
Derive d;
cout << "Base1:" << &b << endl;
vfPtr* vftable = (vfPtr*)(*((int*)&b));
doVF(vftable);
cout << "Base2:" << &b2 << endl;
vftable = (vfPtr*)(*((int*)&b2));
doVF(vftable);
//访问Derive的第一个虚表
vftable = (vfPtr*)(*((int*)&d));
cout << "Derive first vftable:" << &d << endl;
doVF(vftable);
//访问Derive的第二个虚表: 地址偏移
vftable = (vfPtr*)(*((int*)((char*)&d + sizeof(Base1))));
cout << "Derive second vftable:" << (int*)((char*)&d + sizeof(Base1)) << endl;
doVF(vftable);
}
总结: