前言
大家好,这里是YY的带你手把手掌握C++系列。大部分知识点都含有【特性介绍】【使用场景】【注意要点】【易混淆点】【代码演示】【画图演示】由于C++体系之庞大,所以该系列分为合集和分P知识点,本篇博客为合集! 大家可以通过本篇博客查找C++相关知识点和使用场景。欢迎大家收藏,以备以后使用。希望能帮助到大家! 欢迎大家点赞评论,留下您的宝贵意见!对作者而言是莫大的激励!谢谢大家!
本P主要的知识点有:【缺省函数】【命名空间域】【函数重载】【引用】【C++中的NULL与空指针区别】【内联函数】【类】【This指针】【const成员/成员函数】【static静态成员 】【explicit关键字】【友元】【内部类】【匿名对象(即临时对象)】【初始化列表】【类的六个默认成员函数】【C/C++的内存管理】【模板】
另有C语言专栏:涵盖C语言基础与拓展知识,欢迎大家前往阅读!订阅! 【1】指针【2】数组【3】操作符4】动态内存管理【5】内存函数大全【6】文件操作函数 【7】程序的编译链接预处理详解【8】数据在内存中的处理
运用场景:
图示:
(小声说:图有点小糊...保证就这张!)
注意事项:
缺省函数的定义和声明:
引入:在实际运用代码的过程中,可能存在局部变量之间命名冲突/库与局部变量命名之间相互冲突的情况,因而我们可以在局部域全局域之外设置一个区域:命名空间域。要使用时通过 a)展开命名空间域 / b)指定访问命名空间域 来实现。
程序在编译时的优先顺序:局部域->全局域->展开的命名空间域
【函数重载】
注意点:对 返回值 没有要求 ,注意声明!(例:缺省函数)
图示:(注意函数声明时,是否存在缺省函数等问题)
引用,即取别名。它的最大特点是编译器不会为引用变量而开辟空间,他们共用同一块空间。
1.引用使用时必须要初始化。 2.引用在初始化时引用一个实体后,不能再次引用其他实体,只能赋值。 3.引用使用起来更安全。
图示:
作为输出型参数时,面对大对象/深拷贝对象时,由于不用另外开辟空间拷贝,可以提高效率
1.适用场景 2.修改返回值+获取返回值 (使通讯录代码更简洁)
适用场景:(静态区栈帧不销毁)
原本操作:
改进后操作:
权限相关知识点:【权限等级较高的是const和具有常属性的量,权限较低的是普通数据】
图示:
1.平级和权限缩小的情况
2.权限平移情况
3.权限放大情况
PS:const原则上不能修改,但是可以通过找到其空间直接修改。(指针/别名)
图示:
1.C++兼容C,C语言中的结构体strcut也算是一种类,是public(公有)的,可以被类外直接访问。 2.类中的函数默认是内联函数,具体是否是内联函数编译器会判断。如果将其定义和声名分开,即类放在.h文件,定义函数放在.cpp文件,函数不为内联函数。
类由访问限定符划分,类中既有成员变量,又有成员函数
计算类的大小,只用考虑成员变量的大小 例如:上图中,类的大小为8字节
PS:内存对齐,本质上是牺牲空间换取效率。通过调整默认对齐数可以对这一过程进行动态调整。
没有成员变量的类对象,需要1byte,是为了占位,表示对象存在
This指针本质是形参,所以this指针是和普通参数一样存在函数调用的栈帧里
void Print(Date* const this)
{
cout << this->_year << "-" << this->_month << "-" << this->_day << endl;
}
void Print()
{
// this不能在形参和实参显示传递,但是可以在函数内部显示使用
//this = nullptr;
cout << this << endl;
cout << this->_year << "-" << _month << "-" << _day << endl;
}
重点注意:p->Print()并非解引用操作
注意点:Print的地址不在对象中
将const修饰的“成员函数”称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this 指针,表明在该成员函数中不能对类的任何成员进行修改
图示:
PS:权限知识点在下方
用const修饰this指针的好处:普通对象和const对象都能调用 图示:
权限相关知识点:【权限等级较高的是const和具有常属性的量,权限较低的是普通数据】
图示:
答案:不是的,要修改成员变量的函数不能加。
请思考下面的几个问题:
声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用static修饰的成员函数,称之为静态成员函数。
使用要点:静态成员变量一定要在类外进行初始化
请思考下面的几个问题:
步骤:
图示:
原理:
代码演示:
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;
}
用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:构造函数不仅可以构造与初始化对象,对于单个参数或者除第一个参数无默认值其余均有默认值的构造函数,还具有类型转换的作用
图示:
PS:涉及到权限知识点(可见同博客【三.const.权限知识点】)
图示:
引入:友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。 友元分为:友元函数和友元类
通俗而言:友元函数的声明表达了友元函数能够访问这个类的权限,相当于客人(友元)函数拥有主人家的钥匙(友元声明),可以随便进出主人家里,偷吃主人家里的饼干(访问私有域成员) 。但是一个屋子有太多钥匙不太安全,故不要多给钥匙(友元不宜多用)
友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加friend关键字
说明:
代码演示:
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类中私有的成员变量则不行。
代码演示:
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;
};
概念:如果一个类定义在另一个类的内部,这个内部类就叫做内部类。内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限。
注意:内部类就是外部类的友元类,参见友元类的定义,内部类可以通过外部类的对象参数来访问外部类中 的所有成员。但是外部类不是内部类的友元。
特性:
特性:
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;
}
引入:构造函数调用之后,对象中已经有了一个初始值,但是不能将其称为对对象中成员变量的初始化, 构造函数体中的语句只能将其称为赋初值,而不能称作初始化。因为初始化只能初始化一次,而构造函数体内可以多次赋值。
初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式。
代码展示:
class Date
{
public:
Date(int year, int month, int day) 初始化列表
: _year(year)
, _month(month)
, _day(day)
{}
private:
int _year;
int _month;
int _day;
};
缺省值与初始化列表的关系: (下列代码中 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,缺省值是给初始化列表的
但是如果初始化列表中显式定义,则以初始化列表为主
};
尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化。
成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关
图示:
当没有显式定义(我们不主动写时),编译器会自动生成
构造函数的主要任务是初始化对象,如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义,编译器将不再生成。
需要自己写的情况:
不需要自己写的情况:
注意!!!
构造函数可以用重载的情况:
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;
}
析构函数的主要任务是清理对象
默认析构函数:与默认构造函数类似,编译器对内置类型成员不做处理,对自定义类型会去调用它的析构函数。
需要自己写的情况:
不需要自己写的情况:
行为:在创建对象时,创建一个与已存在对象一模一样的新对象
拷贝构造函数:
已知类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指针指向一块新开辟的空间。此时需要自己写拷贝构造函数。
浅拷贝的缺陷:(默认拷贝构造运用引用防止死递归的后遗症)
运算符重载:
例:转化演示
注意:
赋值运算符重载格式:
引入: 内置类型取地址时有取地址操作符,而自定义类型呢?于是出现了取地址重载。它用到的场景非常少,可以说取地址重载——补充这个语言的完整性,更加系统。
这两个默认成员函数一般不用重新定义 ,编译器默认会生成
这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比如不想让别人获取到指定的内容! (设为nullptr)
代码演示:
class Date
{
public :
Date* operator&()
{
return this ;
// return nullptr;让普通成员的this指针不被取到
}
const Date* operator&()const
{
return this ;
}
private :
int _year ; // 年
int _month ; // 月
int _day ; // 日
};
图示:
图示:
PS:C的内存管理有malloc/calloc/realloc/free(可见博主C专栏:动态内存管理)
引入:C语言内存管理方式在C++中可以继续使用,但有些地方就无能为力,而且使用起来比较麻烦,因此C++又提出了自己的内存管理方式:通过new和delete操作符进行动态内存管理。(一般C与C++内存管理不混用)
注意:申请和释放单个元素的空间,使用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;
使用场景:
代码演示:
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;
}
使用场景:有一个自定义类型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根本区别:
实例分析:(顺序)
图演示:
代码演示:
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;
}
函数模板代表了一个函数家族,该函数模板与类型无关,在使用时被参数化,根据实参类型产生函数的特定类型版本。
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)
引入:用不同类型的参数使用函数模板时,称为函数模板的实例化。模板参数实例化分为:隐式实例化和显式实例化。实例化实现的任务是交给编译器的。
引入:隐式实例化的机制是让编译器根据实参推演模板参数的实际类型,而这往往会出现一些问题
适用情况:其交换的两者是同一类
不适用情况:其交换的两者不是同一类
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类型而报错
解决方式:
显式实例化:在函数名后的<>中指定模板参数的实际类型
代码演示:
int main(void)
{
int a = 10;
double b = 20.0;
// 显式实例化
Add<int>(a, b);
return 0;
}
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;
}
面向对象和面向过程的对比: 面向对象更注重对象与对象之间的关系和交互——现实世界类和对象映射到虚拟计算机系统。例:商家,骑手,用户之间的关系 面向过程更多指的是实现目的过程步骤:上架->点餐->派单->送餐 通俗而言:即对象与事的区别
面向对象的三大特性:封装,继承,多态
1.访问限定符(C++实现封装的方式)
2. 在C++语言中实现封装 封装本质上属于一种管理。例:计算机设计中的开机键,usb插口等等,让用户通过这些和计算机交互。而实际工作的是硬件元件。 在C++中实现封装,可以通过类和操作数据的方法进行结合,通过访问权限(访问限定符)来隐藏对象内部实现细节,控制哪些方法可以在类外部直接被使用。 例:在设计通讯录的项目中,往往会建立结构体Steplist,以及各种增删查改的函数。但是使用者可以同时通过函数和通过修改结构体来实现功能,就会造成使用上的差异性(比如需要区分某个变量top表示的是末元素还是末元素的下一个区域)。C语言阶段通讯录写法:
C++运用类以后的写法: