2万字长文结合诸多案例终于完成了对C++全面的基础排查以及总结,通过朱有鹏老师C++课程的学习,自己的实践总结,对C++所有的关键字进行系统性的了解。
本文主要介绍了但是不局限与;详细介绍
C++的四种cast、[reinterpret_cast、const_cast 、static_cast 、dynamic_cast]
面向对象的一系列关键字以及其实战virtual虚函数,override重写,final终止继承、using还原访问权限、friend友元、explicit显示调用
const成员函数以及mutable打洞
static在c中和C++新增的静态方法与静态全局变量的差别
union共用体测试大小端
C方法的内存对齐,以及C++引入的内存对齐,和查看类型的关键字typeid
主要用于模板的静态断言static_assert
等等都结合个人学习理解以及大量案例佐证的,希望各位大佬多多指正,
有很多细节部分会在剩下的详细讲解C++面向对象中进行总结。
1、char;字符类型,一般占1字节、
注意;从C++14开始char默认是unsigned还是signed取决于目标平台,如arm默认unsigned,而X64默认是signed,建议如果在意符号最好显式使用unsigned char或signed char
若是signed型,就意味着取值范围为[-128,127];
若是unsigned型,就意味着取值范围为[0,255];
2、wchar_t宽字符;用于应对一个字符编码超过1字节的Unicode编码,与char没有太大的区别,只是char只占1个字节,而wchar_t占多个字节,导致可以存储的字节数不同,wchar_t和char的数组都能存下unicode码,区别是需要几个单元才能存一个字,wchar_t具体多少个要看具体实现可能是unsigned short也可能是int。
并且)wchar_t都有要用wcin和wcout来输入输出,对应字符串为wstring
3、指定具体字节数的字符类型
(1)char8_t (C++20 起) char16_t (C++11 起) char32_t (C++11 起)
(2)这三个类型一个套路,最大特征就是明确指定占用字节数,且都是无符号的
(3)char8_t很大程度上等同于unsigned char
(4)关于char8_t可以参考:https://stackoverflow.com/questions/57402464/is-c20-char8-t-the-same-as-our-old-char
(5)C++20起,新增字符串类u8string, u16string, u32string
剩余一些不太复杂的关键字;了解即可
1、C++中无明显变化的关键字
if
else
for
do
while
break
continue
switch
case
default
goto
return
unsigned
signed
float
double
short
int
long
void
sizeof
register
volatile
extern;声明在外部定义的函数或变量,C++中多了一个显示告诉编译器使用gcc编译,extern “c”{},这个在"C++学习 -4 C和C++的混合编程及库的调用"讲述得很清楚
C++学习 -4 C和C++的混合编程及库的调用
typedef
asm;内嵌汇编代码
2、C++中新增的运算符代用关键字
(1)逻辑运算代用关键字
and &&
or ||
not !
(2)位运算代用关键字
bitand &
bitor |
xor ^
and_eq &=
or_eq |=
xor_eq ^=
compl ~
(3)不等判断运算符代用关键字
not_eq !=
(4)运算符代用关键字的优势:有些人认为这样便于理解和阅读
3、C++的bool关键字
(1)bool类型也叫逻辑类型,是个2值enum,值为true或false(这2个也是C++关键字)
(2)C语言没有bool关键字,不源生支持bool类型,一般用typedef int bool;这样来自定义
(3)C++语言源生支持bool类型,一般占1字节(平台相关),用法没什么差异
(4)bool内建和自定义至少有一个差别:函数重载机制认为bool是不同类型
1)引用类似于linux中符号链接、还要链接类型与引用类型保持一致,并且C++引入引用的目的是由于指针太危险了(可以直接修改),引用就是功能弱化安全性增加的底配板指针,其本质就是
const类型的指针,例如
int &a = b;等效于 int * const a = &b,根据c高级的知识可以理解为指针a指向的地址不能改变了,但是指向地址内的值可以改变。因为本质是这种地址不能改变了,因此也就有了引用的一个规则;定义时必须初始化,因此相比指针弱的地方也就是更安全的地方也就是定义时必须初始化并且后面不能修改了(也就是不能二次关联其他变量)从而也就不会有野指针这回事。
2)引用在c++中主要用在函数传参和返回值地方;优势与指针一样传参不需要进行拷贝而是指向的,并且可以结合const引用来表示这个函数接口是否内部修改这个变量,如果只是只读则修饰为const(也叫输入型参数),需要修改的则直接传入引用,这样也是便于接口的调用者和实现者的协作(关于使用者和实现者的协作有很多机制再面向对象里面会讲解到)
3)引用和sizeof关键字;sizeof引用得到的大小是引用关联变量的大小,这个在面向对象的时候更有体会。
1)在C语言中assert是运行时中断程序并向stderr打印信息,并且在运行时频繁调用极大影响运行效率,并且表达式出错会中断程序,因此一般很少使用,很累赘。
表达式为;void assert(experssion表达式),experssion为假则出错中断
2)在C++中就引进了静态断言static;主要是在编译期间来进行判断
表达式;void static_assert(布尔常量表达式 , 消息)在C++11引入的。
作用;其主要实现编译时的静态断言,表达式为假则编译报错并打印后接着的消息,表达式为真则没有影响。
在C++中static_assert主要使用在用于检查模板参数是否合乎期望,在编译期间先一步的给模板抽象先检查一下,编译错误比运行错误更容易解决,帮助程序员检查代码传入的参数。
扩展;C++20中引入了concept关键字,来进一步的实现模板参数在编译时的类型匹配检查,其比static提示信息,定位更准确
标准库介绍链接
https://zh.cppreference.com/w/cpp/language/static_assert
3)案例
直接static_assert检查定义的类是否符合表达式
在模板类中使用static_assert判断传入参数的合法性
基础使用模板template 和 typename 如何定义模板类以及定义模板类实类
直接static_assert检查定义的类是否符合表达式,
在模板类中使用static_assert判断传入参数的合法性
assert.h文件
#ifndef __ASSERTTEST__
#define __ASSERTTEST__
class Myclass
{
public:
Myclass();//重写构造函数则需将默认空的也定义
Myclass(char c);
char mychar;
};
class MyEmptyClass
{
public:
void func(char c);
};
//std::is_empty 判断类是否为空类
//(没有任何非静态成员变量,也没有虚函数,具体查看cppreference.com)
//保证MyEmptyClass为空
static_assert(std::is_empty<MyEmptyClass>::value, "empty class needed");
//Myclass不为空
static_assert(!std::is_empty<Myclass>::value, "empty class needed");
//MyTemplate模板类,里面直接使用T, U
//定义的时候直接MyTemplate temp_my(myClass,myEmptyClass);使用
//Myclass MyEmptyClass为模板类的模板参数
//temp_my为MyTemplate类的对象,
//(myClass,myEmptyClass);对应其构造函数
template <typename T, typename U>
class MyTemplate
{
//确保模板T是一个非空的类
static_assert(
!std::is_empty<T>::value, "T should be a non_empty class"
);
//确保模板U是一个空的类
static_assert(
std::is_empty<U>::value, "U should be an empty class"
);
public:
MyTemplate(){};
MyTemplate(T t, U u)
{
this->t = t;
this->u = u;
}
void f()
{
u.func(t.mychar);
}
T t;
U u;
};
#endif
assertTest.cpp文件
#include
#include "assertTest.h"
using namespace std;
void MyEmptyClass::func(char c)
{
cout << c << endl;
}
Myclass::Myclass()
{
}
Myclass::Myclass(char c)
{
mychar = c;
}
int main()
{
Myclass myClass('c');
MyEmptyClass myEmptyClass;
MyTemplate<Myclass, MyEmptyClass> temp_my;
//定义模板类实类
MyTemplate<Myclass, MyEmptyClass> temp_my1(myClass,myEmptyClass);
//MyTemplate temp_my;
temp_my1.f();
return 1;
}
1)在C语言中就存在的enum,其使用为
定义;enum day_old{Mon1, Tur1, Men1};
使用;enum day_old d1_old = Mon1;
一般优化为 typedef enum day_old{Mon1, Tur1, Men1}day_old;使用typedef将enum day_old类型重命名为day_old则使用的时候直接day_old d1_old即可,不需要带enum了,在struct中通常这样使用
2)在C++中与struct类似,同样不需要typedef重命名使用的时候可以不带enum了,
因为枚举存在两个冲突问题;不同枚举之间不能有相同的名字,枚举值不能与宏的名字一样,不然都会有冲突。
扩展;在C++11中引入可以带类前缀的枚举,解决了第一个冲突问题,因为使用的时候需要带上前缀因此不会有冲突问题,但是第二个与宏的问题没有解决
注意enum枚举类型与数据类型强转的时候是没有做保护的,枚举是有限范围的,如果超出范围也不会报错,使用需要使用的时候自己注意了
自己的一个小实验,进行预处理,enum还在的,没有被处理,则表示enum是数据类型,而不像宏一样在预处理会被处理
#include
using namespace std;
//C++11扩展的可以指定类和数据类型(可省略)
//enum class day:unsigned int{Mon, Tur, Men};
enum class day_new{Mon = 2, Tur, Men};
typedef enum day_old{Mon1, Tur1, Men1}day_old;
int main()
{
//验证enum与数值转换,与数组类似是没有保护的
//直接+100超出定义的枚举范围也没有问题,还可以强转回给enum
day_new d = (day_new)((unsigned int)day_new::Mon + 100);
if(d == day_new::Men)
{
cout<<"day_new = Men"<<endl;
}
cout<<(unsigned int)d<<endl;
/*
day_new d1 = day_new::Mon;
//enum day_old d1 = Mon1;C中需要带上enum
//C++里面则不需要带上enum,跟struct类似
day_old d1_old = Mon1;
if(day_new::Mon == d1)
{
cout << (unsigned int)d1 << endl;
}
*/
return 0;
}
该部分只片面介绍了一些关键字的用法,关于具体的面向对象思想以及相关特性以及解决什么问题在后续博客中仔细分析
1)继承机制;c++中使用:来表示,而有其他高级语言使用extends来表示继承
2)virtual虚拟关键字; 表示虚拟的,一般与动态运行时确定机制相关,如虚函数,虚继承
3)override 重写关键字;在成员函数声明或定义中,override 说明符确保该函数为虚函数并覆盖某个基类中的虚函数。若此非真则程序为谬构(生成编译错误)。
作用;只是为了明确表明我是重写父类的虚函数方法,增加库制作者和调用则的协作
编写案例时出错,顺便拿出来说一下,多态重载和隐藏覆盖
4)继承中止final ;指定某个虚函数不能在子类中被覆盖,或者某个类不能被子类继承。
一个class不希望被继承(不想当父类了),则定义的时候用final修饰,则如果再去继承用final修饰的类那么编译器则会报错。如果一个虚函数成员方法不希望被子类重写,override则可以声明时用final修饰表示不能被重写,否则报错。
在其他高级语言中也有final这个关键字,c++是c++11标准推出的。
final修饰虚函数
final修饰类
5)using关键字;两个用法
1、命名空间的大包含声明
using namespace介绍
2、用于重新定义继承是访问权限问题
首先得先了解面向对象类的权限管控以及继承引入的权限问题,使用using可以局部对访问权限局部打洞还原回之前的权限级别;注意只能找回不能升级放大权限。
注意;using只能用于private和protect继承中的权限损失还原找回来,如果方法在父类中本就是private的那么在子类中也没有办法有using访问到的
6)oprator 用于运算符重载时定义方法名时使用。如如果要重载加号+的运算符,那么其重载函数名则要定义为oprator+这样编译器才会认识这是对于+号运算符的函数,否则不认识,并且编译器寻找运算符重载函数也是去类方法中去找oprator加运算符名字的成员方法。
/*
以下代码涉及;
运算符重载的两种方法;外部函数声明为友员方法来实现重载,成员函数来实现重载
赋值运算符;c++编译器会提供默认的隐式赋值运算符函数,因此不写也没有问题
有一种情况会隐式操作创建临时变量并调用拷贝构造函数
拷贝构造函数;注意c++编译器会提供默认的浅拷贝构造函数,如果显示定义拷贝构造函数一般就会认为是需要深拷贝的,这里只是为了验证那种隐式调用拷贝构造函数的存在
构造函数;有其他构造函数则需要将默认构造函数显示实现,可以用初始化列表的形式初始化成员
*/
#include
#include
using namespace std;
class A
{
private :
int a;
public :
A(){a = 0;};//写了其他构造函数则要写默认构造函数
A(int a1);
~A(){};
void print();
//外部函数方式实现运算符重载;通过友员函数访问成员
friend A operator+ (const A& other1, const A& other2);
//成员函数方式的运算符重载
//A operator+ (const A& other);
//赋值运算符函数重载只能以成员方式的形式重载,因为它存在默认重载函数,如果还有友员形式则会产生歧义
A& operator= (const A& other);
//拷贝构造函数
A(const A& Other);
};
void A::print()
{
cout << "A::print() a = " << this->a << endl;
return ;
}
A::A(int a1):a(a1)//初始化列表的方法初始化成员
{
}
/*
//成员函数方式的运算符重载
//实现对象A的+号运算符重载函数 返回值 = this + other
A A::operator+ (const A& other)
{
A a_temp;
a_temp.a = this->a + other.a;
//这里面有一个创建临时对象并调用拷贝构造函数返回的隐藏步骤
//根据代码实践验证,上面说的调用拷贝构造函数不对,没有调用,实践是返回*this的时候才会有临时变量调用拷贝构造函数这回事
return a_temp;
}
*/
A operator+ (const A& other1, const A& other2)
{
A a_temp;//局部的,函数结束内存也就释放了
a_temp.a = other1.a + other2.a;
//这里面有一个创建临时对象并调用拷贝构造函数返回的隐藏步骤
//根据代码实践验证,上面说的调用拷贝构造函数不对,没有调用,实践是返回*this的时候才会有临时变量调用拷贝构造函数这回事
return a_temp;
}
//这个不写也是可以成功赋值的,因为存在默认的赋值运算符重载
A& A::operator= (const A& other)
{
this->a = other.a;
//返回对象;这里面才是有一个创建临时对象并调用拷贝构造函数返回的隐藏步骤
//如果返回A& A::operator= (const A& other)的是引用,则没有创建临时变量以及调用拷贝构造函数这回事情
return *this;
}
//拷贝构造函数
//这里显示重写只是想验证如果返回对象以及对象是*this的时候会隐藏创建对象并调用拷贝构造函数赋值的步骤
//根据这里的实际隐式调用拷贝构造的前提是返回对象而非引用,返回*this而不是局部对象
A::A(const A& Other)
{
this->a = Other.a;
cout << "A& A(const A& Other)" << endl;
}
int main()
{
A a;
A a1(9);
A b;
b = a + a1;//C++默认都对每个自定义class都提供了默认=赋值运算符函数
b.print();
return 0;
}
引入的一个函数返回时隐藏调用拷贝构造函数的问题
自己理解;解释一下为什么返回对象的函数为什么返回this会调用拷贝构造函数而返回普通局部的对象则不会调用拷贝构造函数
首先明确this的时候创建了临时对象并调用拷贝构造对齐进行赋值,但是之前理解错误,认为是因为成员函数要返回对象,而this和局部变量不能被直接返回,this实际不是存在的对象,局部对象更加不能被返回直接调用,而认为他们都应该去创建临时变量去返回。其他这些理解是错误的,
理解是错误的,函数传参返回值是有特定结构的帧栈结构来管理的,而不需要我们去管局部对象不能被返回需要去创建临时变量这一套,是有帧栈管理,从而可以直接返回对象和局部变量的。因此也就有了返回局部对象的时候没有去创建临时对象调用拷贝构造函数,但是为什么this会创建临时变量和调用拷贝构造呢?因为this表示当前对象,而当前对象所在的内存并不在帧栈结构中,而函数返回需要放到帧栈结构中进行管理从而统一实现返回,那么C++编译器需要将*this创建一个临时变量并调用拷贝构造函数进行赋值存放到帧栈中,这样就有了调用拷贝构造函数的步骤。
下面是朱有鹏老师的解答
7)friend 友元
他引入的目的就是解决一个class外部的函数也能访问class内部受保护的成员变量也就是private类型的变量这是友元函数,友元类就是批量创建友元函数的一种方法,将这个类的所有方法都变成友元方法,都可以去访问那个类的私有成员变量。
友元其实是对面向对象的扩展,对封装性的一种破坏,打洞。
具体实现,上面运算符重载中就有运用。
引入一个案例;两个类的成员方法不能互为对方的友元函数,因为需要前置声明,但是前置声明并不能访问具体属性,就是A类方法需要调用b类的属性,但是b类的也有方法需要调用a类的属性,如果不为友元则没有问题。
两个类的成员方法互为友元,涉及到前置声明以及需要利用前置声明来访问属性则报不完全类型访问的编译错误
8)explicit 显示关键字
用来修饰只有一个参数的构造函数,以阻止构造函数不喝事宜的隐式的类型转换
至于为什么要加入这个,不让存在隐式转换,因为有这样的需求。例如
引用其他博主的解释;https://www.cnblogs.com/cutepig/archive/2009/01/14/1375917.html
与模板泛型相关的一些细节和语法特性以及技术使用在后面在仔细阐述,这里只做基本了解
1)temlate<>用于定义一个模板
2)typename 用于表示用什么名字来暂时代替占位
模板实际是一种抽象,C++高级编程特性就是不断向抽象化发展,具体在第三方在细说
,目前第一部分暂时先介绍一个大概。
3)export;专用于模板,类似于extern作用于简单类型一样的作用,用于导入。
用来在cpp文件中定义一个模板类或者模板函数,而它的声明在对应的.h文件中在其他文件cpp中使用,需要export进行导入。
在其他高级语言中java使用import来导入类库。具体要用来在看就行了,先了解有这个东西
4)requires C++20引入的 用于表示模板的参数约束,具体要用来在看就行了,先了解有这个东西
1、exception 异常 运行时错误,非正常情况下,如果出错那么程序就可能奔溃,但是如果引入异常处理机制,则是在出现异常之前进行判断并捕获不然异常发生,走相关处理流程,而不再继续原来流程(原来的流程会触发异常),这样程序就不会终止,而是被采取其他方式执行或者正常退出
注意;try包含的代码越少代码的效率也就越高,因为try里面的代码都需要时时刻刻监视,这需要耗费效率的。
最常见的异常处理典型案例就是除0
异常处理的用法
try{
//认为有可能出错的代码,
//如果是自定义的异常那么需要自己throw抛出对应异常,
//如果不是自定义异常那么系统编译器会返回系统已定义的异常,则不需要手动抛出throw。
}catch(写捕获什么异常)
//或者该异常或要做什么处理,之后就又继续执行后面流程
}
代码实践
//分析了
//C语言如何处理除0的异常情况,
//c++使用系统定义异常如何处理
//C++使用自定义异常如何处理
#include
#include
#include
using namespace std;
int main()
{
cout << "please input 2 numbers" << endl;
int a, b;
cin >> a >> b;
/*
//直接执行
//则除0直接异常退出,提示 Floating point exception (core dumped)
cout << a/b << endl;
/*
//在c语言中是这样处理,特殊if判断一下这样来常规代码处理异常
if(b == 0)
{
cout << "b = 0 ,unable do function" << endl;
return -1;
}
cout << a/b << endl;//代码判断没有问题再执行
*/
//在c++语言使用try检测 catch捕获 throw抛出的异常机制处理
/*
在c++语言使用系统自带的异常,则不需要手动throw抛出,只用捕获就行了
try{
cout << a/b << endl;//代码判断没有问题再执行
}
catch(exception)//经过很多测试和查询,暂时不知道除以0返回的异常为什么异常,但是系统自定义返回的异常我们之间catch就可以了,不用去throw抛出
//... 使用...表示任何类型的异常也没有用
{
cout << "b = 0 ,unable do function" << endl;
return -1;
}
*/
//在c++语言使用自定义异常,则需要自己手动判断条件并抛出,在catch的时候会去匹配对应的类型进行处理,如果没有对象的则仍然异常奔溃
try{
if(b == 0)
{
//throw -1;//则可以进行捕获
throw 'a';//则提示报错,因为没有catch捕获字符类型的异常,
//错误提示为;terminate called after throwing an instance of 'char' Aborted (core dumped)
}
cout << a/b << endl;//代码判断没有问题再执行
}
catch(int)//经过很多测试和查询,暂时不知道除以0返回的异常为什么异常,但是系统自定义返回的异常我们之间catch就可以了,不用去throw抛出
{
cout << "b = 0 ,unable do function" << endl;
return -1;
}
//剩余代码
cout << "other code" << endl;
return 0;
}
从上面可以看出C++自定义异常去抛出捕获和C语言常规代码处理是一样的逻辑,那么为什么还需要引入异常机制呢?
原因是;异常处理机制相比c语言的常规代码自己去判断做保护更加集中,集中对部分可能错的代码一起进行监控判断,并统一进行捕获处理,例如一段代码中有几处需要判断被除数不能为0,如果为0那么就要提示重新输入,如果C语言的常规代码判断处理那么就需要写多个判断以及同样的提示重新输入的语句,而C++的异常机制则可以省略后面一部分,如果自定义类型则也需要去判断,但是只需要抛出异常即可,之后统一进行捕获来输出提示重新输入的语句,而不是每一处都要添加提示重新输入,这只是简单的比较。
转载介绍一篇介绍异常很详细的文章;
https://www.cnblogs.com/wkfvawl/p/10816156.html
2、异常与函数相关的基础知识
在一个函数中throw抛出一个异常后,如果没有catch匹配捕获到对应的异常,则会一层层向外传递,直到被catch为止,并且向外传递的过程中如果没有try对应的catch中没有匹配到则函数也不会往下执行而是抛出的时候直接退出该函数,返回上层函数,都是这样的操作直到有函数catch到,如果能够catch到则当前函数会继续执行,如果最后整个程序都没有catch到则会被操作系统拦截并引发奔溃。
库函数一般就都是蒋异常抛出,让类库的调用者自己去catch处理。这才有了向外传递的实际作用,将库函数的实现者和库函数的调用者关于异常处理部分进行分开。由库函数调用则调用引发的异常理应由调用者去处理对应的异常返回。
3、用于函数抛出异常的throw列表
void func() throw(A,B,C);这种声明就是告诉库函数的调用者func有可能抛出这3钟异常,也只可能抛出这三种一起,不会抛出其他的异常了。
这就是函数可以用throw列表来标识自己会抛出的异常;
void fun() throw(); //表示fun函数不允许抛出任何异常,即fun函数是异常安全的。但是在C++11中引入了noexcept关键字来标识没有异常了。
void fun() throw(…); //表示fun函数可以抛出任何形式的异常。
void fun(); //没有throw列表表示函数可能会抛出任意类型的异常,因此可以看出throw列表只是起到一个显示作用,显示的告诉调用者,让他调用库函数的时候知道关于这个库函数内部的一些异常情况,这也是实现库函数的人和调用库函数人的一种沟通方式
void fun() throw(exceptionType); // 表示fun函数只能抛出exceptionType类型的异常
noexcept关键字与函数抛出throw()作用是一致的都是用于表示函数不会抛出任何异常,是安全的,由C++11引入
具体细节用法可以查看cpp标准库网站
C++17 起,throw() 被重定义为严格等价于 noexcept(true)。具体细节查看标准库介绍
CPP标准库网站关于noexcept介绍
4、标准库中的exception类,标准异常,则是c++中专门定义的一些异常的集合,在很多库的内置代码中的错误就会抛出这些异常,而是这就是不需要去自定义的异常而是系统定义的
如bad_typeid,使用 typeid 运算符时,如果其操作数是一个多态类的指针,而该指针的值为 NULL,则会拋出此异常
如bad_cast,用 dynamic_cast 进行从多态基类对象(或引用)到派生类的引用的强制类型转换时,如果转换是不安全的,则会拋出此异常
1)首先介绍const关键字,从c和c++两种语言的使用方面介绍
在c语言中const一共有3种用法
1、const修饰变量;表示常量并且必须定义的时候初始化,值不能再改变了,常与宏来比较,只是比宏多了一个静态类型检查,注意的是在c语言中const变量可以通过指针的方式绕过编译器去修改,但是在c++因为其const修饰变量的处理机制不一样,所以通过指针是无法修改的,因为c++是采用常量表的结构来存储const变量,一一绑定的,如果要使用const类型的变量的地址那么c++会创建一个临时变量来进行操作,但是原本const变量还是没有变化的。其中下面关键字;cast
类型转换 C++的四种cast中的3)const_cast 用来修改const或者volatile属性的转换
,也是在编译时起作用的。对这个有相关介绍,这里就不在阐述了。
2、const修饰数组,和常量一样
3、const修饰指针;注意用*号分割,查看到底是修饰指向的值不可变还是指针不可变(不可以重新指向其他变量地址);
在C++语言中向前兼容c的那3钟用法并且新增了两种用法
1、const类型的引用;常用于函数传参来限定函数内部不对实参进行修改,同时也有告知的意思。告诉调用类库的人,实现者以及编译器,你看到这个函数声明形参为const引用类型那么就表示我这个函数不会去修改这个值。
2、const修饰成员函数;int func()
const;//const成员函数来明确告知func函数不会修改这个类的任何一个成员变量的值,注意实现的地方也要加const。
区分;
int func(int &a);
int func(const int &a);
int func(int &a)const;
注意;const的保护只可读不可改写都是编译器层次的保护,通过报错来保护,但是是实际在内存的角度都是可读可写的,这一点要非常明确。
2)介绍几个与const相关的关键字,mutable,constexpr
1、mutable;因为由于const修饰的成员函数是不可以修改其中任意一个成员变量,但是有时候我们又有需求,如有几十个成员变量,但是我这个方法只用修改其中的一个,那么我又想提高效率声明为const成员函数,这个时候就需要“打洞”,仍然修饰为const类型成员函数但是我可以修改指定的成员变量,其他的我还是不能修改,这个时候就引入了mutable来解决这个问题。
mutable;用来突破const成员函数中不可修改成员变量,实现单点突破,打洞的思想,从而允许const方法中可以直接修改修饰对应的成员变量。
2、constexpr关键字,C++11引入的
constexpr 说明符声明 可以 在编译时求 得 函数 或变量的 值。 然后这些 变量和函数(若给定了合适的函数实参)即可用于仅允许编译时常量表达式之处。
本质上就是;让程序在编译时计算,利用编译时的计算能力,增加运行时的效率,inline也是提供运行时效率。
用法;constexpr int multiply (int x, int y)
{
return x * y;
}
const int val = multiply( 10, 10 ); // 将在编译时计算,编译后直接变成了const int val = 100;则运行时不用再计算,提供运行时的效率,
注意;由C++11引入,但是实际有一些编译器并不支持,需实际测试
3、C++20新引入的2个
(1)constinit https://zh.cppreference.com/w/cpp/language/constinit
(2)consteval https://zh.cppreference.com/w/cpp/language/consteval
1)在c中static修饰静态全局变量和函数,作用就是限制链接属性保证只有当前文件有效,C++中向下支持这种做法,但是更推荐使用namespace命名空间的机制。
2)在c中static修改局部变量,更改地址域到全局区,更改生命周期到整个文件,但是作用域还是在局部范围内。c++中继续沿用
3)C++新扩展的static用在class中表示静态成员属性和静态成员方法,则静态的属于类class而不是对象的,直接通过类名来调用,静态成员类似于静态的全局变量,但是将作用域限定到了类作用域中。
静态类往往用在单例模式中。静态类和单例都是实际和面向对象思想有所差异的特殊情况下使用的。
4)this 是c++内部定义的 不是语言定义的,而是C++编译器内部帮我实现的,本质是放到c++class内部执行将来创建对象本身的一个指针
作用就是让我们在未定义对象前可以在方法中调用对象里的成员 this->a这样既可
1)struct
在c语言中struct是用户自定义的类型,主要功能是对功能相关数据的封装打包,强调的是数据,并且c中的struct是不能直接封装函数的,但是传统上可以通过函数指针来间接表达封装函数;
为什么一直强调数据,方法;因为写程序=数据+算法,那么就是数据和方法了。
2)C++中struct就是class的初级阶段,class在struct基础上做了很多扩展,便有了面向对象,可以说class可以做的c++中的struct都可以做,只是有一部分特性不一样。
C++中struct和class的区别;总而言之的目的是为了向下兼容以及class是从struct过度来的,因此初期的struct的一些改变需要兼容
如果没有多态和虚拟继承,在C++中,struct和class的存取效率完全相同,可以替换的,
默认继承权限不同,class继承默认是private继承,而struct默认是public继承
class还可用于定义模板参数,像typename,但是关键字struct不能同于定义模板参数
3)类Class是对属性(成员变量)和方法(成员函数)的封装,而struct强调数据的封装、
封装的一个很重要的特性就是访问权限管控;本质是为了隐藏实现的具体细节,避免意外篡改,总而言之是为了保护。
一共有三个级别;public protect private 从不严格到严格
4)c++对象的创建和销毁;
new和delete本质就是变量对应的内存地址的分配和释放,就是与内存空间的绑定解绑。其是malloc和free的升级版。从普通变量升级到对象做的改变,主要是能否自动调用构造和析构函数的区别,还有返回值的区别吧。
可以查看别人总结的文章
转载分享的new和malloc的区别
5)struct和class的区别
C和C++中struct的区别
(1)C中不支持成员函数(只能通过函数指针成员变量间接支持),而C++源生支持。
(2)C中不支持static成员,而C++中支持。后面会详细讲,C++ static class是一个大知识点
(3)访问权限,C中默认public,C++中默认public,但是可以显式指定public/private/protected三者之一
(4)继承特性上,C中不支持(只能通过结构体包含来间接实现),而C++源生支持,且struct和class可以互相继承
(5)初始化方面,C中靠初始化式(gcc扩展了初始化语法),而C++靠构造函数所以初始化更自由可定制化
C++中struct和class的区别
(1)默认成员权限,struct默认public,class默认private
(2)继承关系的权限管控,struct默认public,class默认private
(3)struct和class交叉继承时,默认的权限管控取决于子类而不是基类
(4)模板相关使用都用class,而不用struct了
总结
(1)C++中struct和class差别不大,大多数情况下都可以直接替换使用而不出错
(2)C++中struct其实有点“人格分裂”,他既要兼容C中struct,又要像C++的class
(3)结论:除非是只需要打包几个变量数据就用C方式的struct,否则如果需要面向对象式编程就用class
1)auto
在c语言中和C++11标准之前auto都是表示局部变量,可以省略的,但是在c++11标准后增加了新的意义;自动推导变量类型,就是在编译时根据初始化值来确定的,则方便我们在定义的时候不用直接指定数据类型,更重要的是我们不好在写代码的时候就确定类型,需要运行时推导,如在泛型模板抽象类编程中就有很大帮助,这个时候我们是无法当时确定数据类型的,因此auto可以根据初始化右值来自动推导。
auto可以一次定义多个变量,但是这些变量需要是同一类型的。
2)decltype
C++11标准引入的,这个不是自动推导,而是指定表达式类型,拿已知类型或者已知类型变量来推导类型从而定义变量。注意decltype可以不用赋初值,但是auto必须赋初值才能推导,
decltype(已知类型或者已知类型的变量) j;
3)两者的区别
(1)auto忽略顶层const,而decltype则保留const
(2)auto作为类型占用符,而decltype用法类似于sizeof运算符
(3)对引用操作,auto推断出原有类型,decltype推断出引用
(4)对解引用操作,auto推断出原有类型,decltype推断出引用
(5)auto推断时会实际执行,decltype不会执行,只做分析。
C++是一种强类型语言,对数据类型十分在意,因此在编写代码的时候很好利用类型相关的特性能够很好地提供程序的正确性。
c语言中关于类型转换只有两种;一个是隐式类型转换、一个强制类型转换
而c++提供了四种类型转换就是要很好的处理类型转换问题,其实就是处理好程序员需要的类型转换和告知编译器我需要做什么转换,编译器那边需不需要管类型转换过程中的检测报错。
而c语言就没有这种机制,隐式就直接编译器检测,不对就报错,而强制类型转换就编译器不用管报错,直接强转。
c++就提供了多种,就是让程序员自己定义转换类型让编译器配合更好的达到类型检查的目的,保证程序的正确性。
1)reinterpret_cast 转换 通过重新解释底层位模式在类型间转换。要编译器放弃严苛的类型检查,
2)const_cast 转换 添加或移除 const
3)static_cast 转换 进行基本转换;告知编译器我的确是要这样转,不要抱警告,但是如果有错可以报错
4)dynamic_cast 转换 进行有检查的多态转换
显式转型 在类型间自由转换
标准转换 从一个类型到另一类型的隐式转换
1)static_cast 静态类型转换 进行基本转换
1;效果与隐式类型转换相同,只是隐式转换可能编译器会报出警告,告诉你这里有类型转换要注意,可能会有问题,而static_cast就不一样了,static_cast就是程序员就是要这样转换,让编译器知道这个是程序员保证不会有问题的 ,则不要提示警告。
2;用处
用法;static_cast<类型> 变量
用来将void * p转换为具体的指针类型,取回原有的指针类型。
int a = 5;
int *p = &a;
void *p1 = p;//丢失了类型
int *p2 = static_cast p1;//取回原有类型
用法类层次结构中父类和子类之间的指针或引用的转换,注意上行转换是安全的(子类转父类),下行转换是不安全的(父类转子类,因为如果转换后去调用子类独有的方法属性则会出错)
3;总结
static_cast<>()是编译时静态类型检查,使用static_cast可以尽量发挥编译器的静态类型检查功能,但是并不能保证代码一定“正确”(譬如可能会丢失精度导致错误,可能经过void *之后导致指针类型错误,可能下行转换导致访问错误。)
注意:static_cast不能转换掉expression的const、volatile、或者__unaligned属性。
2)reinterpret_cast 转换 通过重新解释底层位模式在类型间转换。
1;在内存机制上用不同类型的方法来进行解析,强制明确告诉编译器,我就是要这样做,对错我来负责,你不要管,高级版的static_cast因为static_cast只是告知我要这样做,但是编译器还是会报错 ,但是reinterpret_cast就是直接要编译器不要管,有错也别报错,因为他是要编译器直接从内存去解析,直接让其解析这块内存的解析方法变化了。
如 int i = reinterpret_cast 2.1; 本来浮点数的存储是按照浮点数解析的存取方式存储在内存中的,但是reinterpret_cast转换则直接让编译器在解析的时候直接修改读取内存的解析方法,用int解析的存取方式去取。从底层转换。
2;总结;它纯粹是一个编译时指令,指示编译器将 表达式 视为如同具有 新类型 类型一样处理。要编译器放弃严苛的类型检查,
3)const_cast 用来修改const或者volatile属性的转换 ,也是在编译时起作用的。
无论在c还是c++中有const类型的指针直接赋值给没有带const类型的指针是编译报错的。
1;c++中const有符号表机制
在c高级中可以通过指针绕过编译器检查去修改const常亮,但是在c++中就不行了,并且常量的地址和指向常量地址的指针的值都不不一样,因为指针根本指向的不是常量那个地址,而是临时的内存块,修改的也是那块临时内存的值,而并非真正常量值,因为c++对const做了一套机制,const有专门的表,只要定义为const类型则将const变量名和值绑定在一起类似于宏,并且如果调用取地址则会构建一个临时内存来返回。
用例实践
但是我在c++中也是这样的,
在我这个编译器出现问题,下面博客讲解很清楚,
建议;不建议修改const变量的值,即使修改也要熟悉当前使用的编译器对于该 未定义行为 是如何解释的。
https://blog.csdn.net/weixin_43387091/article/details/107467122
地址一样是因为;C++中的常量折叠:指const变量(即常量)值放在编译器的符号表中,计算时编译器直接从表中取值,省去了访问内存的时间,从而达到了优化。
未加volatile时取出的值不一样是因为编译器的优化处理,还是从符号表取值而不是内存中去取,而加入volatile则表示该变量是易变的则需要每次从内存取值。汇编代码可以看出流程。
2;const_cast<>()一般是用于函数传参的时候使用。
如函数形参为int strcmp(const char *p1, const char *p2);则实参传入的时候需要加入const_cast转换一下再传入进来。
4)dynamic_cast 动态转换是在运行的时候才进行转换的。
编译器运行时有一套机制,用来判断是否能转,如果能转则返回转换后的对象反之失败返回NULL,因此在dynamic_cast使用的时候要先判断NULL是否转成功。
只用于父子指针或引用访问时的转换,尤其是下行转换的时候,
运行时确定对象类型RTTI(run time type indentification)是一种需求,C++有一套机制来实现
关于CPP标准库关于dynamic_cast 的使用
5、四种cast总结;
C中一般都用隐式转换或强制类型转换解决,本质上是一种一刀切方案,全靠程序员自己把控从而c++中引入4种cast转换实际上是细分了具体场景,让程序员在具体情况下显式的使用相应的cast来转换,让编译器和运行时尽可能帮程序员把关。
第三方标准库中相关介绍;
1)作用;typeid 用来返回一个变量(或对象的)类型,由于c++是强类型语言并且有严格的数据类型检查机制,则typeid可以很好的先判断出数据类型,保证程序的正确性。
用法;在typeinfo头文件中,
2)一个表达式变量的类型确定可以分为静态类型和动态类型;分别对应编译期确定和运行时确定(因此在运行时时确定,我们可以通过typeid来确定对应类型判断走正确的分支,保证程序的正确性)
3)typeid的主要用途实践还是在多态并结合指针引用才能发挥作用;
注意;
第三方标准库中有介绍,需查看
typeid的第三方库介绍
验证
还有;
若对处于构造和销毁过程中的对象(在构造函数或析构函数之内,包括构造函数的初始化器列表或默认成员初始化器)使用 typeid,则此 typeid 所指代的 std::type_info 对象表示正在构造或销毁的类,即便它不是最终派生类。
该点也可以自己编写代码验证即可。
1)为什么C++要引入nullptr这个关键字
在c中我们用NULL来标记判断是否是野指针,也就是0,
在c中NULL的本质其实是void*(0),万能指针可以强转为任意类型。在c语言中有时候会int * p = 0;直接这样来初始化是没有问题的,因此
c++要兼容c语言也应该支持这种做法,但是因为c++支持函数重载,则在函数传参过程中传入NULL也就是0那么就会存在歧义;例如
2)从而在c++11就引入了nullptr关键字,单纯的表示指针类型可以转换为其他类型指针。
3)nullptr的本质;C++11引入的
nullptr传参真正表示一个指针
nullptr不属于任何一种对象指针,但是却可以表示任何类型的空指针
本质源码
//暂时还看不懂
const class nullptr_t{
public:
template<class T> inline operator T*()const {return 0;}
template<class C, class T> inline operator T C::*() const {return 0;}
private:
void operator&() const;
} nullptr={};
4)疑问;网上有说这种源码的,但是验证也不对,如果c++的null为0 那么只有int传参岂不是要调用int函数,但是结果是报警告无法编译
/* Define NULL pointer value /
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else / __cplusplus */
#define NULL ((void )0)
#endif / __cplusplus /
#endif / NULL */
可以查看标准库相关介绍
第三方标准库中nullptr介绍
1)传统的内联函数与宏的区别,
2)建议;内联函数最好定义在.h文件中,编译多cpp文件使用,直接包含头文件就行了,但是如果只有一个cpp文件使用则都可以直接定义为静态内联函数,反之只在这个文件中用则可以把函数链接属性设置为当前文件有效。
inline是升级版的宏,也是原地展开,不用花费去函数调用等耗时操作,但是在编译期间进行参数校验后展开,并且inline是建议编译器在编译的时候优化为原地展开不用去调用,但是只是建议当计算器环境不予许的时候也会失效而还是进行函数调用的方式执行。
inline函数声明;不需要加inline,定义的时候加上就可以了。
递归函数,超过一定长度的内循环函数都不应该定义为inline函数
3)C++中inline新增的特性;
在class模板定义声明的时候直接定义了一个函数则这个函数就默认为inline函数,如
class A
{
public:
//在类模板定义声明的时候就直接实现函数体,
//则默认为inline函数,不采用调用方式执行而是原地展开
int add(int a, int b){return a+b;};
};
注意如果不这样,要将inline函数的声明定义分开,则要保证定义声明在同一个.h文件中,要保证编译器能够找到展开,如果跨文件定义了则编译无法置换的。
1)union 共用体、联合体;基本用法与c一致,结构体多选一的关系,例如
union myUnion
{
int a;
char b;
};
union myUnion u1; //c中与emun和struct一样,需要带上union 和类型来定义变量,C++可以省略
myUnion u2;//C++可以省略
u1.a = 1;//共用体可以通过赋值给其中一个变量,用另外一个变量来取,这就是共用体
cout<<u1.c<<endl;//本质就是以int类型的编码存入到u1的内存地址,然后以char类型的编码格式去解析u1内存去取。
根据union的特性可以推论出共用体的大小应该是共用体成员中占内存最大 的那个。
2)根据union的特性可以应用于测试大小端的情况;
如0x1234 则0x12为高字节0x34为低字节,而如果系统为大端存储方式则高地址存储高字节,小端存储方式则高字节存放在低地址。
证明;
先反汇编 objdunmp -d a.out > a.i 查看变量0x123456存放地址在哪里
在使用winhex查看二进制工具具体查看值与内存的关系。都能够证明本机系统为大端模式
3)C++11中的union扩展;编译时需要加入 -std = c++11
在union类型定义后使用的时候不用再加union了,与enum和struct一样的
在c++中union还可以包含对象,但是不能有太复杂的构造解析。
在c++中一切类型都是类 ,uinon也是一样的,c++也把他封装成了一个类,但是他把默认的构造析构函数删除了,因此如果uinon包含对象的时候则需要自己自定义构造析构函数,否则无法定义uinon实例,如图
包含对象的union使用方法如下,注意这里union构造函数为空了,则数据是乱码,并且uinon构造函数只能对其中一个元素初始化的,与存放普通变量一样的,union就是多选一的结构,初始化也只需要只能初始化一个就行了,其还可以设置访问权限,默认与struct一样是public,自己可以设置其他访问权限private、protect;
注意;
包含对象的时候有几点需要注意 union可以拥有成员函数(包含构造函数和析构函数),但是不能有虚函数。
union不能参与继承,不能成为基类也不能成为子类。
union的成员对象不能为引用类型。
4)匿名共用体,就是不写uinon类名直接定义变量,一般用在内置class中作为只允许类成员访问的一个共用体变量。
1)C语言内存对齐相关有两种;
#pragma pack(n)直到#pragma pack()设置还原回去范围定义的都是这个方式对齐
和
__attribute__方法设置对齐,作用域指定类型,而不是一个范围,他还带了两个属性packed属性表示请凑对齐按1一个字节对齐、aligned()指定只对齐,往大成功,往下的失败了;不同机器可能是不一样的,别深究。
2)C++11中引入了两个关键字alignof测试类型以什么对齐方式,alignas 类似于__attribute__也是用域指定类型,而不是一个范围,不同的时它直接设置值而没有那两个属性
3)为什么需要内存对齐;什么情况下需要改变他默认对齐方式
往下对齐;放弃效率节约内存的时候
往上对齐,放弃内存提高效率,如MMU,cache的时候根据总线条数一次性传输效率更高
实战引入
C语言的两种设置方式
C++引入的alignof正好测试了#pragma方法设置失败的真实性,的确没有设置成功
C++引入的alignof的正好测试
(1)线程相关:thread_local (C++11 起)
(2)import和module (C++20)
(3)协程相关:
co_await (C++20 起)
co_return (C++20 起)
co_yield (C++20 起)
(4)并发相关:synchronized (TM TS)
(5)反射相关:reflexpr (反射 TS)
(6)其他:
transaction_safe (TM TS)
transaction_safe_dynamic (TM TS)
atomic_cancel (TM TS)
atomic_commit (TM TS)
atomic_noexcept (TM TS)