前言
大家好,这里是YY的带你手把手掌握C++系列。大部分知识点都含有【特性介绍】【使用场景】【注意要点】【易混淆点】【代码演示】【画图演示】由于C++体系之庞大,所以该系列分为合集和分P知识点,本篇博客为合集!
大家可以通过本篇博客查找C++相关知识点和使用场景。欢迎大家收藏,以备以后使用。希望能帮助到大家!
欢迎大家点赞评论,留下您的宝贵意见!对作者而言是莫大的激励!谢谢大家!
本P主要的知识点有:【缺省函数】【命名空间域】【函数重载】【引用】【C++中的NULL与空指针区别】【内联函数】【类】【This指针】【const成员/成员函数】【static静态成员 】【explicit关键字】【友元】【内部类】【匿名对象(即临时对象)】【初始化列表】【类的六个默认成员函数】【C/C++的内存管理】【模板】
另有C语言专栏:涵盖C语言基础与拓展知识,欢迎大家前往阅读!订阅!
【1】指针【2】数组【3】操作符4】动态内存管理【5】内存函数大全【6】文件操作函数
【7】程序的编译链接预处理详解【8】数据在内存中的处理
目录
1.编译器对This指针的处理本质——不允许修改this,但是允许修改this指向的值
2.This不能在形参和实参显示传递,但是可以在函数内部显示使用
五.static应用:"实现一个类,计算程序中创建出了多少个类对象"
一.易错点:数组存储字符串和指针指向字符串,解引用后所在的位置不同(含例题)
四.new/delete与malloc/free的底层区别(自定义类型演示)
五. new/delete与malloc/free在使用失败时的区别
一.缺省函数
运用场景:
- 例:在通讯录项目时,可以省去初始化函数,通过参数的变化可以选择实现初始化/增删查改。
图示:
- (图中的StackInit函数就是缺省函数,当我们传入栈的地址时且没有传入第二个参数时,它会默认传入另一个参数4)
(小声说:图有点小糊...保证就这张!)
注意事项:
- 只有参数的后部参数才是可以缺省,即缺省函数参数后不可以再跟正常参数
缺省函数的定义和声明:
- 缺省函数只能放在函数声明中——编译器必须在使用函数之前知道缺省值
二.命名空间域
引入:在实际运用代码的过程中,可能存在局部变量之间命名冲突/库与局部变量命名之间相互冲突的情况,因而我们可以在局部域全局域之外设置一个区域:命名空间域。要使用时通过 a)展开命名空间域 / b)指定访问命名空间域 来实现。
程序在编译时的优先顺序:局部域->全局域->展开的命名空间域
三.函数重载
【函数重载】
- 是函数的一种特殊情况,C++允许在同一作用域中声名几个功能类似的同名函数显著特征:这些同名函数的形参列表(个数,类型,类型顺序)不同
注意点:对 返回值 没有要求 ,注意声明!(例:缺省函数)
图示:(注意函数声明时,是否存在缺省函数等问题)
四.引用
1.含义与特点
引用,即取别名。它的最大特点是编译器不会为引用变量而开辟空间,他们共用同一块空间。
2.引用和指针的区别(主要)
1.引用使用时必须要初始化。
2.引用在初始化时引用一个实体后,不能再次引用其他实体,只能赋值。
3.引用使用起来更安全。
图示:
3.引用的实际使用
一.引用作为参数
作为输出型参数时,面对大对象/深拷贝对象时,由于不用另外开辟空间拷贝,可以提高效率
二.引用作为返回值
1.适用场景
2.修改返回值+获取返回值 (使通讯录代码更简洁)
1.不适用场景:
适用场景:(静态区栈帧不销毁)
2.实际应用
- 在通讯录中,用传统的方法,需要“查找"到对应pos位置后再“修改”
- 而运用“引用作为返回值”,可直接对查找到的值进行修改。
原本操作:
改进后操作:
3.引用过程中的权限问题(平移,缩小,放大)
- 首先我们要知道,临时变量是具有常性,const 修饰的类型也具有常性,static的数据存储在静态区同样具备常性。
- const和static的权限理论上平级,而临时变量的权限低于二者。
- 只能存在权限平级和权限缩小的情况,不能存在权限放大的情况。通俗而言:权限低的不能给权限(常性)强的取别名。
权限相关知识点:【权限等级较高的是const和具有常属性的量,权限较低的是普通数据】
- 权限高的或平级的可以给另一量取别名/取地址(权限的缩小和平移)
- 权限低的不能给另一量取别名/取地址(权限的放大)
图示:
1.平级和权限缩小的情况
![]()
2.权限平移情况
3.权限放大情况
PS:const原则上不能修改,但是可以通过找到其空间直接修改。(指针/别名)
五.C++中的NULL与空指针区别
- 在C++中,NULL表示“ 0 ”,实际是一个宏。
- 在C++中要表示 空指针,使用 nullptr。
图示:
![]()
六. 内联函数
1.内敛函数适用“短小,使用频繁的函数”
- 当实现加法功能时,需要频繁调用加法函数,调用函数的过程中包含着频繁地开辟栈帧空间和关闭空间,会让程序运行速度变低。而解决类似问题可以使用“宏函数”,但是宏函数面临——易出错(需要括号确保直接替换后不受影响)。在这时使用内联函数,不会频繁开辟空间,大大提高了程序的运行速度。顶中顶有没有!!但别急,它也还有缺点呢~
2.内敛函数的缺陷/特点
- 内联函数的本质是通过牺牲空间换时间,运用内联函数程序的运行速度大大提升,但于此同时程序的大小也会急剧增大。因此面对一些逻辑稍微复杂的运算(循环/递归)便会大大造成冗余。
- inline对于编译器也仅仅是一个建议,最终是否成为inline,编译器自己会判断。
- 且默认debug状态下,inline不会起作用。
3.内联函数声明和定义必须要放在一起的原因
- 编译器一旦把某个函数作为内联函数处理,就会在其调用的位置展开,即该函数没有地址,源文件编译后不会形成符号表,没有链接冲突。同时也不能在其他源文件中调用,故一般都是直接在源文件中定义内联函数——可以在同一个项目不同的源文件中定义函数名相同但实现不同的inline函数。
七.类
1.C++兼容C,C语言中的结构体strcut也算是一种类,是public(公有)的,可以被类外直接访问。
2.类中的函数默认是内联函数,具体是否是内联函数编译器会判断。如果将其定义和声名分开,即类放在.h文件,定义函数放在.cpp文件,函数不为内联函数。
1.类的组成与计算类的大小(含结构体内存对齐规则)
类由访问限定符划分,类中既有成员变量,又有成员函数
![]()
计算类的大小,只用考虑成员变量的大小
例如:上图中,类的大小为8字节
PS:内存对齐,本质上是牺牲空间换取效率。通过调整默认对齐数可以对这一过程进行动态调整。
2. 空类的大小
没有成员变量的类对象,需要1byte,是为了占位,表示对象存在
3. This指针
This指针本质是形参,所以this指针是和普通参数一样存在函数调用的栈帧里
1.编译器对This指针的处理本质——不允许修改this,但是允许修改this指向的值
void Print(Date* const this) { cout << this->_year << "-" << this->_month << "-" << this->_day << endl; }
2.This不能在形参和实参显示传递,但是可以在函数内部显示使用
void Print() { // this不能在形参和实参显示传递,但是可以在函数内部显示使用 //this = nullptr; cout << this << endl; cout << this->_year << "-" << _month << "-" << _day << endl; }
3.例题对比:传入空指针时,this的运作状况
重点注意:p->Print()并非解引用操作
注意点:Print的地址不在对象中
4.const成员/成员函数
将const修饰的“成员函数”称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this 指针,表明在该成员函数中不能对类的任何成员进行修改
图示:
一.用const修饰this指针的好处——含权限知识点
PS:权限知识点在下方
用const修饰this指针的好处:普通对象和const对象都能调用
图示:
权限相关知识点:【权限等级较高的是const和具有常属性的量,权限较低的是普通数据】
- 权限高的或平级的可以给另一量取别名/取地址(权限的缩小和平移)
- 权限低的不能给另一量取别名/取地址(权限的放大)
图示:
二.能否所有的成员函数都加上const?
答案:不是的,要修改成员变量的函数不能加。
三.几个的使用场景
请思考下面的几个问题:
- 1. const对象可以调用非const成员函数吗?no
- 2. 非const对象可以调用const成员函数吗?yes
- 3. const成员函数内可以调用其它的非const成员函数吗?no
- 4. 非const成员函数内可以调用其它的const成员函数吗?yes
5.static静态成员
一.静态成员基本知识
声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用static修饰的成员函数,称之为静态成员函数。
使用要点:静态成员变量一定要在类外进行初始化
二.静态成员特性
- 静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区
- 静态成员变量必须在类外定义,定义时不添加static关键字,类中只是声明
- 类静态成员即可用 类名::静态成员 或者 对象.静态成员 来访问
- 静态成员函数没有隐藏的this指针,不能访问任何非静态成员
- 静态成员也是类的成员,受public、protected、private 访问限定符的限制
三.静态成员函数和非静态成员函数的调用关系
请思考下面的几个问题:
- 静态成员函数可以调用非静态成员函数吗?no(无this指针)
- 非静态成员函数可以调用类的静态成员函数吗? yes
四.static的应用:“求1+2+3+...n的和"
步骤:
- 将成员变为静态成员变量
- 利用访问操作符与静态成员函数GetRet()得到_ret(和)
图示:
五.static应用:"实现一个类,计算程序中创建出了多少个类对象"
原理:
- 定义一个静态成员变量_scount,再在类中声明一个访问静态成员变量的静态成员函数GetACount();
- 构造++_scount,析构--_scount;
代码演示:
class A { public: A() 构造函数 { ++_scount; } A(const A& t) 拷贝构造 { ++_scount; } ~A() 析构函数 { --_scount; } static int GetACount() { return _scount; } private: static int _scount; }; int A::_scount = 0; 静态成员变量类外定义 void TestA() { cout << A::GetACount() << endl; ::来访问静态成员变量 A a1, a2; A a3(a1); cout << A::GetACount() << endl; }
6.explicit关键字
一.基本性质
用explicit修饰构造函数,将会禁止构造函数的隐式转换
代码演示:
class Date { public: // 1. 单参构造函数,没有使用explicit修饰,具有类型转换作用 // explicit修饰构造函数,禁止类型转换---explicit去掉之后,代码可以通过编译 explicit Date(int year) :_year(year) {} /* // 2. 虽然有多个参数,但是创建对象时后两个参数可以不传递,没有使用explicit修饰,具有类型转 换作用 // explicit修饰构造函数,禁止类型转换 explicit Date(int year, int month = 1, int day = 1) : _year(year) , _month(month) , _day(day) {} */ 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; }; void Test() { Date d1(2022); // 用一个整形变量给日期类型对象赋值 // 实际编译器背后会用2023构造一个无名对象,最后用无名对象给d1对象进行赋值 d1 = 2023; }
二.相关知识补充:隐式类型转换
类型转换会产生临时变量
PS:构造函数不仅可以构造与初始化对象,对于单个参数或者除第一个参数无默认值其余均有默认值的构造函数,还具有类型转换的作用
图示:
1.为什么加上"引用"无法发生隐式类型转换
PS:涉及到权限知识点(可见同博客【三.const.权限知识点】)
图示:
7.友元
引入:友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。 友元分为:友元函数和友元类
通俗而言:友元函数的声明表达了友元函数能够访问这个类的权限,相当于客人(友元)函数拥有主人家的钥匙(友元声明),可以随便进出主人家里,偷吃主人家里的饼干(访问私有域成员) 。但是一个屋子有太多钥匙不太安全,故不要多给钥匙(友元不宜多用)
一.友元函数
友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加friend关键字
说明:
- 友元函数可访问类的私有和保护成员,但不是类的成员函数
- 友元函数不能用const修饰
- 友元函数可以在类定义的任何地方声明,不受类访问限定符限制
- 一个函数可以是多个类的友元函数
- 友元函数的调用与普通函数的调用原理相同
- 友元函数的声明与函数声明不同,仅仅是表达权限
代码演示:
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; }
二.友元类
说明:
- 友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。
- 友元关系是单向的,不具有交换性。
例:比如下面Time类和Date类,在Time类中声明Date类为其友元类,那么可以在Date类中直接访问Time 类的私有成员变量,但想在Time类中访问Date类中私有的成员变量则不行。
- 友元关系不能传递 (如果B是A的友元,C是B的友元,则不能说明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; };
8.内部类
概念:如果一个类定义在另一个类的内部,这个内部类就叫做内部类。内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限。
注意:内部类就是外部类的友元类,参见友元类的定义,内部类可以通过外部类的对象参数来访问外部类中 的所有成员。但是外部类不是内部类的友元。
特性:
- 内部类可以定义在外部类的public、protected、private都是可以的。
- 注意内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名。
- sizeof(外部类)=外部类,和内部类没有任何关系。
9.匿名对象(即临时对象)
特性:
- 匿名对象的生命周期在当前行
- 匿名对象具有常性
- const+引用 :会延长匿名对象生命周期,生命周期在当前函数局部域
int main() { A aa(1); // 有名对象 -- 生命周期在当前函数局部域 A(2); // 匿名对象 -- 生命周期在当前行 Solution sl; sl.Sum_Solution(10); Solution().Sum_Solution(20); //A& ra = A(1); // 匿名对象具有常性 const A& ra = A(1); // const引用延长匿名对象的生命周期,生命周期在当前函数局部域 A(10); Solution().Sum_Solution(20); string str("11111"); push_back(str); push_back(string("222222")); push_back("222222"); return 0; }
10.初始化列表
一.初始化列表和构造函数的关系
引入:构造函数调用之后,对象中已经有了一个初始值,但是不能将其称为对对象中成员变量的初始化, 构造函数体中的语句只能将其称为赋初值,而不能称作初始化。因为初始化只能初始化一次,而构造函数体内可以多次赋值。
二.初始化列表基本结构
初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式。
代码展示:
class Date { public: Date(int year, int month, int day) 初始化列表 : _year(year) , _month(month) , _day(day) {} private: int _year; int _month; int _day; };
三.初始化列表使用场景
- 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
- 类中包含以下成员,必须放在初始化列表位置进行初始化:
- 引用成员变量
- const成员变量
- 自定义类型成员(且该类没有默认构造函数时 )
缺省值与初始化列表的关系: (下列代码中 int x 有演示)
- 初始化列表没显式定义,缺省值给到初始化列表
- 初始化列表显式定义,以初始化列表为主
代码展示:
class A { public: 内置类型可以放到初始化列表中初始化 A(int a) :_a(a) {} private: int _a; }; class B { public: B(int a, int ref) 必须放到初始化列表中进行初始化 :_aobj(a) ,_ref(ref) ,_n(10) {} private: A _aobj; // 没有默认构造函数 (无参/全缺省/默认生成) int& _ref; // 引用 const int _n; // const int x = 3; 缺省值为3,缺省值是给初始化列表的 但是如果初始化列表中显式定义,则以初始化列表为主 };
四.尽量使用初始化列表初始化
尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化。
五.成员变量在初始化列表中的初始化顺序
成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关
图示:
11.类的六个默认成员函数
当没有显式定义(我们不主动写时),编译器会自动生成
1.构造函数
- 默认构造函数(3种):(1)类自己生成的函数(2)无参 (3)全缺省的函数
- 特征: (不传参就可以调用)
构造函数的主要任务是初始化对象,如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义,编译器将不再生成。
- 运作上看,当对象实例化时,编译器会自动调用它
- 形态上看,其名字与类名相同,且无返回值
- 注意点,构造函数允许重载
一.什么时候需要自己写构造函数?
需要自己写的情况:
- 一般情况下,有内置类型成员,要自己写(否则会初始化成随机值)
不需要自己写的情况:
- 当内置类型成员都有缺省值时,且初始化符合要求,可以考虑让编译器自己生成
- 全部都是自定义类型成员(例如:Stack),可以考虑让编译器自己生成
注意!!!
二.构造函数可以使用重载和不可以使用重载的情况
构造函数可以用重载的情况:
typedef int DataType; class Stack { public: Stack(DataType* a, int n) //特定初始化 { cout << "Stack(DataType* a, int n)" << endl; _array = (DataType*)malloc(sizeof(DataType) * n); if (NULL == _array) { perror("malloc申请空间失败!!!"); return; } memcpy(_array, a, sizeof(DataType) * n); _capacity = n; _size = n; } //调用时可用以用d1,使用上方的构造函数 Stack d1(int, 11); //Stack d1(); // 不可以这样写,会跟函数声明有点冲突,编译器不好识别 Stack d2; //调用时可以用d2,使用下方的构造函数 Stack(int capacity = 4) //构造函数(全缺省) { cout << "Stack(int capacity = 4)" << endl; _array = (DataType*)malloc(sizeof(DataType) * capacity); if (NULL == _array) { perror("malloc申请空间失败!!!"); return; } _capacity = capacity; _size = 0; } /*以下代码仅为完整性 void Push(DataType data) { CheckCapacity(); _array[_size] = data; _size++; } void Pop() { if (Empty()) return; _size--; } DataType Top() { return _array[_size - 1]; } int Empty() { return 0 == _size; } int Size() { return _size; } ~Stack() { cout << "~Stack()" << endl; if (_array) { free(_array); _array = NULL; _capacity = 0; _size = 0; } } private: void CheckCapacity() { if (_size == _capacity) { int newcapacity = _capacity * 2; DataType* temp = (DataType*)realloc(_array, newcapacity * sizeof(DataType)); if (temp == NULL) { perror("realloc申请空间失败!!!"); return; } _array = temp; _capacity = newcapacity; } }*/ private: DataType* _array; int _capacity; int _size; };
构造函数不能用重载的情况:无参调用存在歧义
// 构成函数重载 // 但是无参调用存在歧义 Date() { _year = 1; _month = 1; _day = 1; } Date(int year = 1, int month = 1, int day = 1) { _year = year; _month = month; _day = day; }
2.析构函数
析构函数的主要任务是清理对象
- 运作上看,当对象生命周期结束时,编译器会自动调用它
- 形态上看,其在类名前加上~,且无返回值
- 注意点,析构函数不允许重载。
默认析构函数:与默认构造函数类似,编译器对内置类型成员不做处理,对自定义类型会去调用它的析构函数。
一.什么时候需要自己写析构函数?
需要自己写的情况:
- 有动态申请资源时,需要自己写析构函数释放空间。(防止内存泄漏)
不需要自己写的情况:
- 没有动态申请资源时,不需要自己写,系统会自动回收空间。
- 需要释放资源的对象都是自定义类型时,不需要自己写。
3.拷贝构造函数
行为:在创建对象时,创建一个与已存在对象一模一样的新对象
拷贝构造函数:
- 只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰)
- 在用已存在的类类型对象创建新对象时由编译器自动调用(区分于构造函数)
- 拷贝构造函数是构造函数的一个重载形式
已知类Date,已经有实例化的对象 Date d1; 此时想得到一个和d1一模一样的对象d2; Date d2(d1); 类中若有拷贝构造Date (const Date d); 直接进行调用; d2传给没有显示的this指针,d1传给const Date d; Date d2(const Date d1)
- 拷贝构造函数的参数只有一个且必须是类类型对象的引用
当拷贝构造函数为 Date(const Date &d);//引用 Date(const Date d);//错误写法 Date(const Date &d) { this->_year = d.year; this->_month =d.month; this->_day =d.day; } //this 为d2的指针,d为拷贝的类d1
- 原因:【使用传值方式编译器直接报错,因为会引发无穷递归调用】(错误方式)
一.什么时候需要自己写拷贝构造函数?
默认生成的拷贝构造函数为:浅拷贝
需要自己写的情况:
- 自定义类型必须使用拷贝构造(深拷贝)
不需要自己写的情况
- 内置类型直接拷贝(浅拷贝/值拷贝)
例:Date类中都是内置类型,默认生成的拷贝构造函数为浅拷贝可以直接用;
而Stack类为自定义类型,其中有a指针指向一块新开辟的空间。此时需要自己写拷贝构造函数。
二.默认拷贝构造(浅拷贝)的缺陷:
浅拷贝的缺陷:(默认拷贝构造运用引用防止死递归的后遗症)
4.运算符重载函数
运算符重载:
- 参数类型:const T& (传递引用可以提高传参效率)
- 函数名:关键字operator后面接需要重载的运算符符号
- 函数原型:返回值类型+operator操作符+(参数列表)
例:转化演示
注意:
- 不能通过连接其他符号来创建新的操作符:例如operator@
- 重载操作符必须有一个类类型参数
- 用于内置类型的运算符,其含义不能改变:例如+
- 作为类成员函数重载时,其形参看起来比操作数少一个(因为成员函数的第一个参数为隐藏的this)
- .* / :: /sizeof/ ?: /./这五个运算符不能重载
一.运算符重载函数和构造函数使用区别:
5.赋值重载函数
赋值运算符重载格式:
- 参数类型:const T& (传递引用可以提高传参效率)
- 返回值类型:T& (返回引用可以提高返回的效率,有返回值的目的是为了支持连续赋值)
- 检测是否可以自己给自己赋值
- 返回*this:(对this指针解引用,要符合连续赋值的含义)
- 赋值运算符只能重载成为类的成员函数而不能重载成全局函数(如果重载成全局函数,编译器会生成一个默认运算符重载)
- 用户没有显示实现时,编译器会生成一个默认赋值运算符重载,以值的方式(浅拷贝)逐字节拷贝。(注意点:内置类型成员变量直接赋值,只有自定义成员变量需要调用对应的赋值运算符重载)
6.取地址与取地址重载
引入: 内置类型取地址时有取地址操作符,而自定义类型呢?于是出现了取地址重载。它用到的场景非常少,可以说取地址重载——补充这个语言的完整性,更加系统。
这两个默认成员函数一般不用重新定义 ,编译器默认会生成
这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比如不想让别人获取到指定的内容! (设为nullptr)
代码演示:
class Date { public : Date* operator&() { return this ; // return nullptr;让普通成员的this指针不被取到 } const Date* operator&()const { return this ; } private : int _year ; // 年 int _month ; // 月 int _day ; // 日 };
九.C/C++的内存管理
1.C/C++的内存分布规则
- 栈又叫堆栈--非静态局部变量/函数参数/返回值等等,栈是向下增长的。
- 内存映射段是高效的I/O映射方式,用于装载一个共享的动态内存库。用户可使用系统接口 创建共享共享内存,做进程间通信。(Linux课程如果没学到这块,现在只需要了解一下)
- 堆用于程序运行时动态内存分配,堆是可以上增长的。
- 数据段--存储全局数据和静态数据。
- 代码段--可执行的代码/只读常量。
图示:
一.易错点:数组存储字符串和指针指向字符串,解引用后所在的位置不同(含例题)
- *char2数组所在的位置是栈,是位于代码段(常量区)"abcd\0"的一份拷贝;
- pChar3是一个指向代码段(常量区)"abcd\0"的一个指针变量,由于其具有常性,所以要加上const;
图示:
2.C/C++的内存管理方式
PS:C的内存管理有malloc/calloc/realloc/free(可见博主C专栏:动态内存管理)
引入:C语言内存管理方式在C++中可以继续使用,但有些地方就无能为力,而且使用起来比较麻烦,因此C++又提出了自己的内存管理方式:通过new和delete操作符进行动态内存管理。(一般C与C++内存管理不混用)
一.使用new和delete操作符的使用规范
注意:申请和释放单个元素的空间,使用new和delete操作符,申请和释放连续的空间,使用 new[]和delete[],要匹配起来使用。(如果new后接free,无论是否是对同一块空间的操作,都容易报错)
代码演示:
//报错 int* p3 = (int*)malloc(sizeof(int)); // C int* p4 = new int; free(p4); 对开辟同一块空间操作,不匹配 delete p3; //报错 int* p3 = (int*)malloc(sizeof(int)); // C int* p4 = new int; free(p3); 对开辟不同一块空间操作,不匹配 delete p4;
二.new和delete对内置类型的具体使用场景
使用场景:
- 申请一个int类型空间(调用构造,随机值)
- 申请一个int类型空间,并初始化为10
- 申请10个int类型空间
- 申请10个int类型空间,并分别初始化
代码演示:
void Test() { // 动态申请一个int类型的空间 int* ptr4 = new int; // 动态申请一个int类型的空间并初始化为10 int* ptr5 = new int(10); // 动态申请10个int类型的空间 int* ptr6 = new int[10]; // 动态申请10个int类型的空间,并初始化 int* ptr7 = new int[10]{1,3,4}; delete ptr4; delete ptr5; delete[] ptr6; }
三.new和delete对自定义类型的具体使用场景
使用场景:有一个自定义类型A,他的初始化列表需要传入两个参数
- 申请一个空间给A
- 申请一个4个空间给4个A,分别初始化(多参,不可不完全初始化)
代码演示:
void test() { A* p1 = new A(1,1); delete p2; //错误写法:不完全初始化 A* p2 = new A[4]{ A(1,1),A(2,2),A(3,3)}; A* p2 = new A[4]{ A(1,1),A(2,2),A(3,3),A(4,4) }; delete[] p2; }
四.new/delete与malloc/free的底层区别(自定义类型演示)
new/delete 和 malloc/free根本区别:
- new的底层其实也是malloc,与malloc不同之处在于他会调用拷贝构造
- delete的底层其实也是free,与free不同之处在于他会调用析构函数
实例分析:(顺序)
- 在下图中,new了一个栈Stack,其实底层是先malloc个空间给Stack(自定义类型),再调用它的拷贝构造(_array指向的新空间);
- 当delete栈Stack时,先调用析构函数(free掉刚刚_array指向的新空间),再free掉Stack所处的空间;
- PS:如果不是这样,而是先free掉Stack的空间,那么_array指向的新空间将无法被p1找到,造成内存泄漏
图演示:
五. new/delete与malloc/free在使用失败时的区别
- C++是一门面向对象的语言,处理失败时,不喜欢用返回值,更喜欢用抛异常
- 一般用【try-catch捕捉】
代码演示:
try { do { //p1 = (int*)malloc(1024 * 1024); p1 = new int[1024 * 1024]; cout << p1 << endl; } while (p1); } catch (const exception& e) { cout << e.what() << endl; }
二.模板
1.函数模板
一.函数模板概念
函数模板代表了一个函数家族,该函数模板与类型无关,在使用时被参数化,根据实参类型产生函数的特定类型版本。
二.函数模板的格式
template<typename T> void Swap( T& left, T& right) { T temp = left; left = right; right = temp; } //可识别不同的同种类型交换(例:char与char,int与int,double与double)
PS:typename是用来定义模板参数关键字,也可以使用class(切记:不能使用struct代替class)
三.函数模板的实例化
引入:用不同类型的参数使用函数模板时,称为函数模板的实例化。模板参数实例化分为:隐式实例化和显式实例化。实例化实现的任务是交给编译器的。
1.隐式实例化
引入:隐式实例化的机制是让编译器根据实参推演模板参数的实际类型,而这往往会出现一些问题
适用情况:其交换的两者是同一类
不适用情况:其交换的两者不是同一类
template<class T> T Add(const T& left, const T& right) { return left + right; } int main() { int a1 = 10; double d1 = 10.0; Add(a1, d1); //解决方式:Add(a1, (int)d1);强制类型转换 }
分析:
该语句不能通过编译,因为在编译期间,当编译器看到该实例化时,需要推演其实参类型 通过实参a1将T推演为int,通过实参d1将T推演为double类型,但模板参数列表中只有一个T, 编译器无法确定此处到底该将T确定为int 或者 double类型而报错
解决方式:
- 用户自己强制类型转换
- 显式实例化
2.显式实例化
显式实例化:在函数名后的<>中指定模板参数的实际类型
代码演示:
int main(void) { int a = 10; double b = 20.0; // 显式实例化 Add<int>(a, b); return 0; }
3.模板参数的匹配原则
- 一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数
- 对于非模板函数和同名函数模板,如果其他条件都相同,在调动时会优先调用非模板函数而不会从该模 板产生出一个实例。如果模板可以产生一个具有更好匹配的函数, 那么将选择模板
- 模板函数不允许自动类型转换,但普通函数可以进行自动类型转换
2.类模板
一.类模板的格式
template<class T1, class T2, ..., class Tn> class 类模板名 { // 类内成员定义 };
二.类模板的实例化
类模板实例化与函数模板实例化不同,类模板实例化需要在类模板名字后跟<>,然后将实例化的类型放在<> 中即可,类模板名字不是真正的类,而实例化的结果才是真正的类。
Vector<int> s1; Vector<double> s2;
注意区分:
- 在类中:类名等同于类型
- 在类模板中:类型是类型,类名是类名
例如:在下面代码中,类模板中函数放在类外进行定义时,需要加模板参数列表;在访问类模板时,要用Vector<T>(类型),而不是Vector(类名)
代码演示:
template<class T> class Vector { public : Vector(size_t capacity = 10) : _pData(new T[capacity]) , _size(0) , _capacity(capacity) {} // 使用析构函数演示:在类中声明,在类外定义。 ~Vector(); void PushBack(const T& data); void PopBack(); // ... size_t Size() {return _size;} T& operator[](size_t pos) { assert(pos < _size); return _pData[pos]; } private: T* _pData; size_t _size; size_t _capacity; }; // 注意:类模板中函数放在类外进行定义时,需要加模板参数列表 template <class T> Vector<T>::~Vector()//用类型访问类模板 { if(_pData) delete[] _pData; _size = _capacity = 0; }
三.string类
十.面向对象(含与过程对比)
面向对象和面向过程的对比:
面向对象更注重对象与对象之间的关系和交互——现实世界类和对象映射到虚拟计算机系统。例:商家,骑手,用户之间的关系
面向过程更多指的是实现目的过程步骤:上架->点餐->派单->送餐
通俗而言:即对象与事的区别
十一.面向对象的三大特征 (含类的概念)
面向对象的三大特性:封装,继承,多态
1.封装
1.访问限定符(C++实现封装的方式)
![]()
2. 在C++语言中实现封装
封装本质上属于一种管理。例:计算机设计中的开机键,usb插口等等,让用户通过这些和计算机交互。而实际工作的是硬件元件。
在C++中实现封装,可以通过类和操作数据的方法进行结合,通过访问权限(访问限定符)来隐藏对象内部实现细节,控制哪些方法可以在类外部直接被使用。
例:在设计通讯录的项目中,往往会建立结构体Steplist,以及各种增删查改的函数。但是使用者可以同时通过函数和通过修改结构体来实现功能,就会造成使用上的差异性(比如需要区分某个变量top表示的是末元素还是末元素的下一个区域)。C语言阶段通讯录写法:
C++运用类以后的写法:
十.