1、关键字学习
(1)研究编程语言从关键字出发不适合学习,但是适合查漏补缺
(2)C++全部关键字列表查阅
(3)C++关键字相对C有几种情况:新增关键字、新增语义、语义变化、完全无变化
(4)限于面向对象和STL没学,因此本篇文章不追求全面讲解关键字,重点是从一些核心关键字出发学习C++语法特性,体会C++与C的不同,熟悉C++。
2、C++的bool关键字
(1)bool类型也叫逻辑类型,是个2值enum,值为true或false(这2个也是C++关键字)
(2)C语言没有bool关键字,不源生支持bool类型,一般用typedef int bool;这样来自定义
(3)C++语言源生支持bool类型,一般占1字节(与平台相关),用法没什么差异
(4)bool内建和自定义至少有一个差别:函数重载机制认为bool是不同类型
typedef int bool;
int add(int a, int b);
int add(bool a, int b)
C中,bool和int是一个类型,在C++中不是
在C++中,虽然布尔类型只表示true和false,看起来好像可以用一个位来存储布尔类型的变量,但其实,布尔类型在C++中是占用一个字节的。
bool b = 0;
printf("b = %d\n",b);
b++;
printf("b = %d\n",b);
b = b - 3;
printf("b = %d\n",b);
用C++编译器编译运行后得到的是:
b = 0
b = 1
b = 1
从上面我们也可以看到,布尔类型作为C++中的一种基础类型,是完全可以对布尔类型进行运算的,只不过最后遵循非0值为真,0值为假这条规则。
既然布尔类型作为C++中的一种基本数据类型,那么可以
定义bool类型的全局变量
定义bool类型的常量
定义bool类型的指针
定义bool类型的数组
summary:
在C语言中,没有bool这种类型,但是在C++中,把bool当做一种基本的数据类型,既然是数据类型,那么久可以对bool类型的变量进行运算,只不过最后遵循非0值为真,0值为假这条规则。同时,bool类型作为一种基本的数据类型,也可以用来定义常量,全局变量,指针还有数组
1、char
(1)字符类型,一般占1字节,表示字符(ASCI或unicode字符)
(2)从C++14开始char默认是unsigned还是signed取决于目标平台,如arm默认unsigned,而X64默认是signed,建议如果在意符号最好显式使用unsigned char或signed char
(3)char类型cout输出默认为字符,而int类型cout输出默认为数字
(4)1 == sizeof(char) <= sizeof(short) <= sizeof(int) <= sizeof(long) <= sizeof(long long)
=还是<取决于具体的平台
2、wchar_t
(1)宽字符,用于应对一个字符编码超过1字节的Unicode编码
(2)wchar_t和char的数组都能存下unicode码,区别是需要几个单元才能存一个字符
(3)wchar_t占几个字节取决于具体实现(一般不会是一个字节),可能是unsigned short也可能是int
(4)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
typedef
asm(内嵌汇编)
2、C++中新增的运算符代用关键字
(1)逻辑运算代用关键字
and &&
or ||
not !(逻辑取反)
(2)位运算代用关键字
bitand &
bitor |
xor ^
and_eq &=
or_eq |=
xor_eq ^=
compl ~(按位取反)
(3)不等判断运算符代用关键字
not_eq !=
(4)运算符代用关键字的优势:有些人认为这样便于理解和阅读
#include
#include
#include
using namespace std;
int main(int argc, char *argv[])
{
bool a = 3;
bool b = -1;
string str = "hello world, my cplusplus.";
char c[] = "Always try to see the best in people.";
printf("a = %d.\n", a);
cout << a << endl;
cout << b;
cout << "\n" << endl;
cout << "sizeof(bool) = " << sizeof(bool) << endl;
cout << str << endl;
cout << c << endl;
if (a and b)
{
cout << "a and b are ture.\n";
}
while(not a);
cout << "a\n";
//cout << "not b :" << not b << endl;
//cout << "not a :" << not a << endl;
while((not a) or (not b));
cout << "(not a) or (not b).\n";
cout << "a not_eq b :" << (a not_eq b) << endl;
while(a not_eq b);
cout << "ending!" << endl;
return 0;
}
1、引用介绍
(1)引用的经典案例:实现swap函数实战
(2)引用定义和识别的关键:&符号,注意这里和取地址一毛钱关系都没有
(3)引用符号(注意我没说变量,引用相当于别名)在定义时 必须 同时初始化,以后不能再另外赋值,只能使用
int x = 4, y = 6;
int &b1 = x;
//定义了一个引用符号b1,与x关联,关联后只能与x相关,不可与其他变量关联了,类似于linux的软链接
b1 = y;//相当于x = y;即x = 6;
错误:int &b1; b1 = x;//错误,必须定义的同时初始化
2、引用和指针的对比
(1)指针在C和C++中都有,且使用方法和实现本质完全相同;引用只有C++可用
(2)引用可以理解为功能弱化、安全性增强的低配版指针
(3)引用能做的事指针都能做,但指针能做的事儿引用不一定能做
(4)引用是它指向变量的“别名”,这个是从引用的使用效果角度讲的,对熟悉指针的人反而不好理解“别名”这个词
(5)引用比指针弱的地方就是一个引用定义时绑定了一个变量,后面没法改了
(6)引用比指针强的地方也是没法改,所以不存在"野指针"问题,更安全
(7)引用主要用在函数传参和返回值
1、引用可以加const修饰
(1)const int &b = a; 表示b是a的const别名,无法通过b修改a了
(2)主要用在函数形参中,告诉大家该函数内部不会修改实参的值。用在某些时候我们有一个非const类型的变量,但是我们在某个函数调用的过程中,不希望变量的值在函数内部被修改,这时候就可以用const引用来传参。
2、引用和sizeof运算符
(1)sizeof引用得到的不是引用本身的大小,而是引用指向的目标变量的大小
(2)在struct或class中定义一个引用,再sizeof整个struct或class就会不一样
在这两个里边sizef(引用)是八个字节,加之考虑结构体对齐,就可得知sizeof的结果
3、引用的本质是const指针
(1)int &b = a; 类似于 int * const b = &a;
(2)C++标准并没有规定引用是否占用内存空间,但是大多数编译器都把引用实现为const指针,所以大部分编译器中引用也是要占内存空间的
(3)引用是天然const的,所以定义时必须初始化指向变量,否则就没意义了(const修饰之后不可修改)
(4)引用本质是指针,是地址,所以才能实现传址调用的效果
总结:引用就是指针在定义时增加了把指针变量本身const化
#include
#include
using namespace std;
int swap1(int a, int b);
int swap2(int *pa, int *pb);
int swap3(int &pa, int &pb);
int swap1(int a, int b)
{
int temp = 0;
temp = a;
a = b;
b = temp;
return 0;
}
int swap2(int *pa, int *pb)
{
int temp = 0;
temp = *pa;
*pa = *pb;
*pb = temp;
return 0;
}
int swap3(int &pa, int &pb)
{
int temp = 0;
temp = pa;
pa = pb;
pb = temp;
return 0;
}
typedef struct test1
{
int a;
int &a1 = a;
}mytest1;
typedef struct test2
{
char b;
char &b1 = b;
}mytest2;
int main(int argc, char *argv[])
{
#if 0
int x = 3, y = 4;
int &x1 = x, &y1 = y;
swap1(x, y);
cout << "x = " << x << endl;
cout << "y = " << y << endl;
swap2(&x, &y);
cout << "x = " << x << endl;
cout << "y = " << y << endl;
swap3(x1, y1);
cout << "x = " << x << endl;
cout << "y = " << y << endl;
#endif
#if 1
int t = 1;
char z = 1;
double d = 1.11;
const int &t1 =t;
const char &z1 = z;
const double &d1 = d;
//t1 = 5;//error,不允许被修改
cout << "sizeof(t1)" << sizeof(t1) << endl;
cout << "sizeof(z1)" << sizeof(z1) << endl;
cout << "sizeof(d1)" << sizeof(d1) << endl;
//因为引用本质上相当于一个指针,在64为系统上为8字节,加之结构体对齐访问
//故为两个结构体的大小均为16字节
cout << "sizeof(mytest1)" << sizeof(mytest1) << endl;
cout << "sizeof(mytest2)" << sizeof(mytest2) << endl;
#endif
return 0;
}
// C和C++98等老版本里的写法
//enum day {MON, THU, WEN};
//enum day2 {MON, xxxx, yyy};
//#define MON "55"
//C中习惯用typedef来重命名类型以避免每次类型使用都加enum
//typedef enum {MON, xxx, xx} day;
enum day d1; // 定义了一个day类型的变量,变量名是d1
day d1; // C++中定义时可以省掉前面的enum
1、C++继承C的枚举用法
(1)典型枚举类型定义,枚举变量定义和使用
(2)枚举类型中的枚举值常量不能和其他外部常量名称冲突:举例1宏定义,举例2另一个枚举
即使枚举数据类型中的值实际存储为整数,也不能总是将整数值替换为符号名称。例如,不能使用下面的语句将数值赋值给 student,student是一个枚举变量:
student = 1; //错误
但是,可以使用整数值而不是符号名称来测试枚举变量。例如,以下两个 if 语句是等效的:
if (student == Bill)
if (student == 2)
2、C++11中扩展的枚举
(1)enum class enumType:valueType{one=xx, two, three};
对于其的预处理,会将其变成一个:定义一个命名空间,在其中定义一个枚举
(2)两种简写
eg:
enum class day:unsigned int{MON = 44, THU, WEN};
// 简化写法1
//enum class day{MON, THU, WEN};
// 简化写法2
//enum day{MON, THU, WEN};
(3)解决2个枚举中的重名问题(不同的命名空间,互不影响),但是宏定义仍然不能重名
3、关于枚举的3个小细节
iostream中的输入输出流不接受枚举类型,必须进行强制类型转换。无法通过隐式转换,必须通过显示转换。C++/C都是强制类型转换
d1 = day::MON;
cout << (unsigned int)d1 << endl;
(1)枚举类型和值类型的互相转换,枚举类型不可以使用++,只能间接使用,如下所示:
d1 = (day)((unsigned int)d1 + 100);
(2)枚举类型的前置声明,有时使用在定义之前,所以必须要有 前置声明
eg:
enum class day;//只声明了day是一个enum变量,但不知道其内有哪些成员。使用其中的成员时仍会报错。
(3)枚举类型超出范围访问是否会编译时或运行时报错,不会
//编译时:g++ enum.cpp -std=c++11,低版本的并不支持某些语法格式
#include
#include
#include
using namespace std;
//C语言所支持的语法
enum day1
{
MON,
THU,
WEN
};
#if 0
//C++所支持的语法
enum class day2:unsigned int {
MON = 44,
THU = 55,
WEN
};
#elif 1
//简化写法1
enum class day2{MON, THU, WEN};
#elif 0
//简化写法2
enum day2{MON, THU, WEN};//这种形式下不论使用C还是C++的风格都可以实习其中元素的访问
//但是使用这种方式会与day1冲突,其内的成员名一样,而使用前两种
//在一个命名空间中则不会产生干扰
#endif
int main(int argc, char *argv[])
{
enum day1 day_c;
day2 day_cplus;
#if 1
//适用于C的语法
day_c = MON;
cout << day_c << endl;
if (day_c not_eq THU)
cout << "true" << endl;
#endif
//适用于C++的语法
day_cplus = day2::MON;
cout << (unsigned int)day_cplus << endl;//iostream中的输入输出流不接受枚举类型,必须进行强制类型转换。
day_cplus = day2::WEN;
cout << (unsigned int)day_cplus << endl;
if (day2::MON not_eq day2::WEN)
cout << "very good." << endl;
//枚举类型不可直接使用++,枚举类型超出范围访问是否会编译时或运行时报错,不会
day_cplus = (day2)((unsigned int)day_cplus + 55);
cout << (unsigned int)day_cplus << endl;
return 0;
}
1、C语言中union回顾
(1)union翻译成共用体更合适,而不是联合、联合体
(2)union中所有成员是多选一的关系,这是union和struct的最大差别
(3) 面试笔试常考,必须掌握
2、C++中union和C中不同
(1)C++中union类型定义后使用时可以省去union(和上节enum时一样)
(2)C++中union里成员除了普通的,还可以是对象,但是对象不能包含自定义构造函数、析构函数,简单说就是不能太复杂
(3)C++中经常用到匿名union,一般是内置在class内部做成员变量
3、总结
//union在C++中没有突出变化,主要还是沿用C中使用
union myu // union类型的定义
{
char *p;
void (*p1)(int);
};
p1(); // 通过p1函数指针来调用函数
union myu m1; // C中定义了一个myu类型的变量
myu m1; // C++中定义了一个myu类型的变量
union //没有名字,节省了符号表
{
char *p1;
int *p2;
}m1; // 直接定义了union变量m1
1、C中inline使用关键点强调
(1)inline是一种“用于实现的关键字”,而不是一种“用于声明的关键字”,所以关键字 inline 必须与函数定义体放在一起,而不是和声明放在一起
(2)如果希望在多个c文件中使用,则inline函数应该定义在h文件中(不需要额外声明);如果只在一个c文件中使用,则inline函数可以定义在c文件或h文件中(若定义在c文件时可以声明到h文件中去,声明时可以不加inline)
(3)inline函数在项目中可以多次定义,只要函数体完全相同且在一个c文件范围只定义一次inline函数必须在调用该函数的每个文本文件中定义。当然,对于同一程序的不同文件,如果inline函数出现的话,其定义必须相同。
(4)inline只是一种对编译器的建议而不是强制,所以inline函数不一定真被inline
(5)递归函数不应该被声明为inline(递归函数本身被多调用可能会出问题,比如不当inline处理,而且代码量可能太多使得很乱),超过一定长度(通常是10行)的函数不应该被inline,内含循环的函数不建议被inline
2、C++中inline新增的特性
(1)定义在类声明之中的成员函数将自动地成为内联函数,例如如下代码:
class A
{
public:
void Foo(int x, int y) { ... } // 自动地成为内联函数,即使没有inline关键字
}
(2)如果在类中未给出成员函数定义,而又想内联该函数的话,那在类外要加上 inline,否则就认为不是内联的。值得注意的是:如果在类体外定义inline函数,则心须将类定义和成员函数的定义都放在同一个头文件中,否则编译时无法进行置换。
1、C语言中的NULL
(1)NULL用来标记野指针
(2)NULL在C和C++中的定义为什么不同?因为C++不允许void *隐式转为int *等类型
(3)C++中也可以继续用NULL,但是因为函数重载的引入,NULL传参会带来歧义
// C语言中NULL就是(void *)0
// C++语言中NULL就是0
C++中:
int *p = 0;
int *p = NULL;
可以编译通过,因为C++编译器会进行隐式类型转换
if(!p)//判断指针是否为空
if(0 != p)//判断指针是否为空
{
//指针解引用
}
void func(int a)
{
cout << "func int a" << endl;
}
void func(char *p)
{
cout << "func char *p" << endl;
}
void func(int *p)
{
cout << "func int *p" << endl;
}
func(NULL);NULL在这存在歧义,使得不知道调用那个函数,是0还是隐式转换(int *p = NULL;)
2、nullptr如何工作
(1)nullptr传参,表示真正的空指针,NULL是个宏定义,而nullptr是个关键字
(2)nullptr的本质是一个const类型的对象
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={};
2、nullptr的评价
NULL本质是一个数字0,而nullptr本质是一个指针类型 nullptr就是一个可以绕过C++严格的类型检查的NULL,就是因为C++不允许int *p = (void *)0这样,所以才有了nullptr;
(1)C++11开始可用,注意版本要求
(2)实践中在判断野指针时很多人还是喜欢if (!p)这样···(因为许多人有C的开发经验,习惯了,并且C++又支持了这种写法)
(3)nullptr无法解决char *p和int *p这样的传参重载问题,所以还是有点不完美
(4)nullptr不属于任何一种对象指针,但是却可以表示任何类型的空指针
#include
using namespace std;
void func(int a)
{
cout << "this is int" << endl;
}
void func1(int *a)
{
cout << "this is int *" << endl;
}
int main(int argc, char *argv[])
{
int a = NULL;
char p = NULL;
char *q = NULL;
char *b = 0;
if (!q and !b)
{
cout << "these pointers are clear.<1>" << endl;
}
if (q == nullptr and b == nullptr)
{
cout << "these pointers are clear.<2>" << endl;
}
if (NULL == 0)
{
cout << "NULL = 0" << endl;
}
else
{
cout << "NULL = (void *)0" << endl;
}
func(NULL);
func1(NULL);
func1(nullptr);
return 0;
}
1、C中的断言assert
(1)直接参考:https://www.cnblogs.com/lvchaoshun/p/7816288.html
assert宏的原型定义在
原型定义:
#include
void assert( int expression );
assert的作用是先计算表达式 expression ,如果其值为假(即为0),那么它先向stderr打印一条出错信息,然后通过调用 abort 来终止程序运行。
eg:
FILE *fp;
fp = fopen( "test.txt", "w" );//以可写的方式打开一个文件,如果不存在就创建一个同名文件
assert( fp );//所以这里不会出错
fclose( fp );
(2)C的assert是运行时检测发现错误,而不是编译时
已放弃使用assert()的原因是,频繁的调用会极大的影响程序的性能,增加额外的开销。在调试结束后,可以通过在包含#include
#include
#define NDEBUG
#include
(3)C在编译时 错误用#error来输出
2、C++静态断言
(1)C++引入static_assert(表达式, “提示字符串”)来实现编译时的静态断言
(2)实例演示
#include
int main(int argc, char *argv[])
{
static_assert(6==6, "6==6 error");
//static_assert(5==6, "5==6 error");
return 0;
}
3、静态断言主要用途
(1)static_assert主要用于检查模板参数是否符合期望
(2)C++20中引入了concept来进一步更好的实现模板参数的编译时类型匹配检查
1、C语言中内存对齐关键点
(1)#pragma 和 attribute((packed)) attribute((aligned(n)))
(2)__attribute__是GUN C中极具特设的一大机制,可以用来设置
函数属性(Function Attribute)
变量属性(Variable Attribute)
类型属性(Type Attribute)
attribute((aligned(n))) //采用n字节对齐
attribute((packed)) //采用1字节对齐
attribute((aligned(n)))中,n的有效参数为2的幂值,32位最大为2^32,
64位为2^64,这个时候编译器会将让n与默认的对齐字节数进行比较,取较大值为对齐字节数,与#pragma pack(n)恰好相反。
attribute((packed))则为取消结构在编译过程中的优化对齐,按照实际占用字节数进行对齐,也就是采用1字节对齐。
/******************************************使用格式*******************************************/
/*定义结构体时不对类型重命名*/
struct mystruct
{
/*成员变量定义*/
}__attribute__() /*(可同时在这定义变量)*/;
struct __attribute__() mystruct
{
/*成员变量定义*/
}/*(可同时在这定义变量)*/;
/*定义结构体同时对类型进行重命名*/
typedef struct mystruct
{
/*成员变量定义*/
}__attribute__() MS;
typedef struct __attribute__() mystruct
{
/*成员变量定义*/
}MS;
/*************************************************************************************************/
#pragma pack(n) //作用:C编译器将对以下的结构体按照n个字节对齐。
/*
定义结构体类型
*/
#pragma pack() //作用:取消自定义字节对齐方式。
其中#pragma pack(n)中,n的有效参数为1/2/4/8/16,
这个时候编译器会将让n与默认的对齐字节数进行比较,取较小值为对齐字节数。
在64位操作系统64位编译器的环境下,当n ≥ 8时,内存对齐的字节数是8,不然为n
在32位操作系统32位编译器的环境下,当n ≥ 4时,内存对齐的字节数是4,不然为n
#pragma pack(push,n)与#pragma pack(pop)
#pragma pack(push,n) //作用:把原来对齐方式设置压栈,并设新的对齐字节为n的对齐方式
/*
定义结构体类型
*/
#pragma pack(pop) //作用:恢复原来压栈的对齐方式。
在指令#pragma pack(push,n)里n的一切效果和#pragma pack(n)中是一样的,这里不再赘述。这个指令的优越性在于:他设置新的对齐方式时,会将原来的对齐方式(假设是M)进行压栈保存下来。待#pragma pack(pop)解除#pragma pack(push,n)的作用时,将会对原来的对齐方式进行弹栈恢复,执行之后结构体的对齐方式又变回了M而不是编译器默认的。
2、C++中内存对齐新增关键字
(1)alignof (C++11 起)
alignof用来测定变量或者类型的字节对齐数
(2)alignas (C++11 起)
alianas用来往大改变字节对齐,使用方法类似__attribute__((aligned(n)))
#include
#include
using namespace std;
struct mystruct1
{
int a;
char b;
double c;
}__attribute__((packed)) s1;
struct mystruct2
{
int a;
char b;
double c;
}__attribute__((aligned(8))) s2;
struct alignas(32) mystruct3
{
int a;
char b;
double c;
}s3;
int main(int argc, char *argv[])
{
cout << "sizeof(char)" << sizeof(char) << endl;
cout << "sizeof(int)" << sizeof(int) << endl;
cout << "sizeof(double)" << sizeof(double) << endl;
cout << "alignof(s1) = " << alignof(s1) << endl;
cout << "sizeof(s1) = " << sizeof(s1) << endl;
cout << "alignof(s2) = " << alignof(s2) << endl;
cout << "sizeof(s2) = " << sizeof(s2) << endl;
cout << "alignof(s3) = " << alignof(s3) << endl;
cout << "sizeof(s3) = " << sizeof(s3) << endl;
return 0;
}
3、什么情况下需要人为改变/指定对齐方式
(1)往大去对齐。有时候会有一些硬件特殊要求,譬如MMU,cache等。用__attribute__((aligned(n)))实测ok,用#pragma实测不ok
(2)往小去对齐。有时候需要节省内存而浪费效率,所以希望忽略内存对齐,紧密排放。用#pramgma实测ok,用__attribute__((aligned(n)))实测不ok
1、alignas的用法补讲
(1)使用方法:一般在类型定义时,放在名称前
(2)效果:和__attribute__((aligned(n)))
2、typeid
(1)typeid是一个运算符,类似于sizeof
(2)typeid定义在头文件typeinfo中,必须包含该头文件
(3)typeid用来返回一个变量(表达式)(对象)的类型,类似于C语言中的:typeof() 是GUN C提供的一种特性,它可以取得变量的类型,或者表达式的类型。
(4)typeid使用实战
cout << "a type = " << typeid(a).name() << endl;
// int a; // int类型的type.name是 "i"
// char a; // char类型的type.name是 "c"
// unsigned char a; // unsigned char类型的type.name是 "h"
signed char a; // signed char类型的type.name是 "a"
3、typeid的深层次说明
(1)一个表达式的类型分静态类型(在编译的时候就可以确定类型)和动态类型(在运行的时候才可以确定类型,如C中的void *),分别对应编译期 和 运行时类型决策系统
(2)typeid可用来返回静态类型,也可用来返回动态类型
(3)typeid是C++语言本身的特性,由编译器和库函数共同支撑
(4)typeid真正大用在引入class和继承后,并结合指针和引用后才能显现出来
!!!!对于接下来cast相关课程内容的警告问题,对于不同版本的不同的编译器有不同的结果
1、static_cast
(1)源生类型之间的隐式类型转换,避免警告,转换后可能丢失精度,正确性需要程序员自己保证
(2)用来将void *p转为具体的指针类型,取回原有的指针类型
int a = 5;
int *p = &a;
void *p1 = p; // p1已经丢掉了自己的类型
int *p2 = static_cast<int *>(p1); // p2又取回了自己的类型
int *p3 = (int *)p1;
char *p4 = static_cast<char *>(p1);
(3)用于类层次结构中父类和子类之间指针和引用的转换。其中上行转换时安全的,而下行转换时不安全的。
(4)总结:static_cast<>()是编译时静态类型检查,使用static_cast可以尽量发挥编译器的静态类型检查功能,但是并不能保证代码一定“正确”(譬如可能会丢失精度导致错误,可能经过void *之后导致指针类型错误,可能下行转换导致访问错误。)
eg:
int a = 5444;
char c = a;
char c = static_cast<char>(a);// 有可能有错误
(5)评价:static_cast必须会用,见了必须认识,能理解使用static_cast的意义,但是实际上只能解决很初级的编程问题,属于初级语法特性。
C++中:
int a = 5;
int *p = &a;
char *p5 = static_cast<char *>(p); // 编译器报错
char *p6 = (char *)p; // OK的
char *p5 = reinterpret_cast<char *>(p); //可以的
而C中:
char *p6 = (char *)p;//也是可以的
2、reinterpret_cast
(1)用于明确告知编译器该类型转换在编译时放行,正确性由程序员自己负责
(2)reintepret_cast转换前后对象的二进制未发生任何变化,只是对这些二进制位的编译器类型标识发生了变化,或者说是编译器看待这些二进制位的结论不同了
(3)reintepret_cast一般用于将指针转成int或者回转,将A类型指针转为B类型指针等
/*
// 把一个16写入内存地址为0x530000e0的寄存器中
unsigned int *p = (unsigned int *)(0x530000e0);
*p = 16;
*/
unsigned int *p = reinterpret_cast
(4)reintepret_cast其实就是让C++在本次转换中放弃严苛的编译器类型检查
3、const_cast
(1)用来修改类型的const或volatile属性
(2)格式为:const_cast
const int a = 5;
a = 6; // 编译报错,因为a是const类型所以编译器发现操作非法
int *p = (int *)&a; // 老式转换可以,但是不推荐
int *p = const_cast<int *>(&a); // 新式写法,推荐
*p = 14;
(3)思考:const_cast为什么能修改const为非const?
int strcmp(const char *p1, const char *p2);
char a[] = "dfdfdf";
char *pa = &a;
strcmp(const_cast<const char *>(pa), const_cast<const char *>(pb));
4、dynamic_cast
(1)只用在父子class的指针和引用访问时的转换中,尤其是下行转换时
(2)属于一种运行时转换机制,运行时才能知道转换结果是NULL还是有效对象
(3)运行时确定对象类型RTTI(run time type indentification)是一种需求,C++有一套机制来实现
5、4种cast转换总结
前三种都是在编译时起作用的,最后一种dynamic_cast是在运行时起作用的。
(1)C中一般都用隐式转换或强制类型转换解决,本质上是一种一刀切方案,全靠程序员自己把控
(2)C++中4种cast转换实际上是细分了具体场景,让程序员在具体情况下显式的使用相应的cast来转换,让编译器和运行时尽可能帮程序员把关。
1、auto关键字
(1)auto在C中修饰局部变量,可以省略,完全无用。C++中的auto完全是一个新关键字
(2)auto要求至少不低于C++11标准来支撑
(3)auto在编译器由编译器帮我们自动推导出变量(对象)类型,所以定义时必须初始化右边右值的类型,自动推导出左值的类型。
(4)auto可以一次定义多个同类型的变量,但是不能一次定义多个类型不同的变量,这是auto的类型推导机制决定的。
2、decltype关键字
(1)C++11新增关键字
(2)decltype可以让编译器推导目标表达式的类型作为一种类型符使用
(3)decltype(表达式)作为类型定义变量不要求初始化
3、auto和decltype的对比
(1)auto忽略顶层const,而decltype则保 留const
(2)auto作为类型占用符,而decltype用法类似于sizeof运算符
(3)对引用操作,auto推断出原有类型,decltype推断出引用
(4)对解引用操作,auto推断出原有类型,decltype推断出引用
(5)auto推断时会实际执行,decltype不会执行,只做分析。
#include
#include
using namespace std;
int func(void)
{
cout << "func" << endl;
}
typedef int pf(void);
int main(void)
{
// auto a = func();
decltype(func) a;
if (typeid(a) == typeid(pf))
cout << "haha" << endl;
// cout << "typeid(a) = " << typeid(a).name() << endl;
/*
const int i = 5;
decltype(i) j = 8;
// j = 9; // 报错,因为j是const的
cout << "typeid(j) = " << typeid(j).name() << endl;
*/
/*
const int i = 5;
auto j = i;
j = 4; // 正确
cout << "typeid(j) = " << typeid(j).name() << endl;
*/
/*
double i = 5;
decltype(i) j; // 定义了变量j,类型是和i相同
cout << "typeid(j) = " << typeid(j).name() << endl;
*/
/*
// int i = 5;
auto i = 5; // 编译器在编译时自动帮我们推导出i的类型是int
// int a, b;
// auto a = 4, b = 5; // 正确
// auto a = 4, b = 5.5; // 错误
cout << "typeid(i) = " << typeid(i).name() << endl;
*/
return 0;
}
对于隐去的代码可取消注释,编译进行实验
1、struct和class
(1)struct是C中用户自定义类型,主要功能是对功能相关数据的封装
(2)struct不能直接封装函数,但可以通过封装函数指针来间接封装函数
(3)struct就是class的初级阶段,class在struct基础上做了很多扩展,便有了面向对象
2、访问权限
(1)类是对数据(成员变量)和方法(成员函数)的封装
(2)封装的一个重要特征就是访问权限管控,本质是为了隐藏实现细节,避免意外篡改,而struct没有访问权限管控,其内的成员都可任意访问。
(3)C++支持三个访问管控级别:private、protected、public
3、C++的对象创建和销毁
(1)对象的本质等同于C中的变量,对象的创建和销毁也就是变量的产生和销毁,本质上是变量对应的内存地址的分配和释放归还
(2)C中全局变量和局部变量都是自动分配和回收内存的,堆内存需要用户手工申请和释放(malloc和free调用)
(3)C++中因为变量升级成了对象,涉及到构造函数和析构函数,因此malloc和free升级为了new和delete
(4)C++中仍然大量使用全局变量和局部变量,但是动态分配占比例越来越多。这是业务(完成的任务)特点决定的,不是C++语言决定的。语言只是提供机制,业务才决定策略。
c:偏控制,体量偏小
c++:偏大,数据结构较复杂,动态性较大
1、
(1)静态全局变量和函数,限制链接属性。C++中建议优先使用命名空间机制替代
(2)静态局部变量,更改地址域和生命周期。C++中继续沿用。
2、static在C++中新增用法
(1)用在class中,有静态数据成员和静态成员函数
(2)简单理解:静态成员和方法是属于class的,而非静态是属于对象的
(3)静态类往往用在单例模式中,实际上和面向对象的思想有所差异
(4)要真正理解静态类,得先学习面向对象和普通非静态类后才可以
3、this关键字
(1)本质是个指针,指向当前对象(编译器在编译时帮我们加入到class内部的)
(2)this的主要作用是让我们在未定义对象前可以在方法(函数)中调用对象里的成员
1、面向对象允许类的继承机制
(1)C++中用" : "来表示继承关系,有些编程语言有extends关键字表示继承关系
(2)virtual修饰class的成员函数为虚函数,一般在基类中,只有接口声明没有实体定义
(3)基类的virtual成员可以在派生类中override重写,以实现面向对象的多态特性
(4)注意区分重写override与重载overload
(5)override关键字是C++11引入,用来在派生类中成员函数声明时明确表明需要派生类去重写的那些成员方法,这样如果程序员在成员方法实体定义中做的不对编译器可以报错提醒
2、继承的终止final
(1)一个class不希望被继承(不想做父类),可以定义时用final修饰
(2)一个成员方法不希望被子类override,可以声明时用final修饰
(3)final是C++11引入的
(4)很多其他面向对象语言如java中也有final关键字,也是这个作用
3、using关键字
(1)用法1就是using namespace std;这种
(2)用法2与class的继承和访问权限限制有关,属于一个声明,能够让private继承的子类去声明并访问父类中本来无权限访问的成员
4、operator
(1)用于运算符重载,也就是为一个class重定义某种运算符
5、friend
(1)让不属于一个class的外部函数也能访问class内受保护的成员变量
(2)实际上是对面向对象的一种扩展或者说破坏,在面向对象深入理解之后再来学习更好
6、explicit
(1)本意为显式的,对应implicit隐式的
(2)用来修饰只有一个参数的构造函数,以阻止构造函数不合时宜的类型转换
1、C语言中const用法回顾
(1)const变量,比宏定义的优势是带数据类型,可以让编译器帮我们做类型检查,二者修饰的东西都不可更改
(2)const数组,和常量变量类似
(3)const指针,三种情况:const int *p, int * const p, const int *const p;
int func1(const int *pa)
// 调用时,int i;
func1(const_cast<const int *>(&i));
2、C++中const新增用法
(1)const引用,主要用于函数传参,限制函数内部对实参进行修改
int func1(const int &a)
// 调用时,int i; func2(i);
(2)const成员函数,限制函数内部对类的成员变量的修改
struct A
{
public:
int i;
int func6(void) const ;
int i;
int j;
int func8(int &a); // 这样写,隐含意思就是func8内部很有可能会修改传参a的值
int func9(const int &a); // 这样写,隐含意思就是func9内部不会改变a的值
int func10(int a) const; // const成员,明确告知func10内部不会修改class A
// 的成员变量的值
};
3、mutable
(1)mutable用来突破const成员函数的限制,让其可以修改特定的成员变量
在C++中,mutable也是为了突破const的限制而设置的。被mutable修饰的变量,将永远处于可变的状态,即使在一个const函数中。
(2)案例参考:https://www.cnblogs.com/yongdaimi/p/9565996.html
#include
using namespace std;
class Person{
public:
int getAge() const;/*调用方法*/
int getCallingTimes() const;/*获取上面的getAge方法被调用了多少次*/
private:
int age;
char *name;
float score;
mutable int m_nums;/*用于统计次数*/
};
int Person::getAge() const
{
cout << "Calling the method" << endl;
m_nums++;
return age;
}
int Person::getCallingTimes() const
{
return m_nums;
}
int main(int argc, char *argv[])
{
Person *person = new Person();
for(int i = 0; i < 10; i++){
person->getAge();
}
cout << "getAge方法被调用了" << person->getCallingTimes() << "次" << endl;
delete person;
getchar();
return 0;
}
4、constexpr
(1)用法如下:
constexpr inline int multiply (int x, int y)
{
return x * y;
}
const int val = multiply( 10, 10 ); // 将在编译时计算
const int val = 100;//效果与上边的相同
(2)本质上是让程序利用编译时的计算能力,增加运行时效率,
inline也可提高运行时的效率
(3)由C++11引入,但是实际有一些编译器并不支持,需实际测试
5、C++20新引入的2个
(1)constinit https://zh.cppreference.com/w/cpp/language/constinit
(2)consteval https://zh.cppreference.com/w/cpp/language/consteval
1、模(mu)板编程初体验(结合自己所写的程序便于理解)
(1)template和typename
(2)模板实际上是一种抽象,C++的高级编程特性就是不断向抽象化发展
template <typename T>
T add(T a, T b)//T就被当作这个模板的泛型数据类型
{
return (a+b);
}
2、export
(1)用来在cpp文件中定义一个模板类或模板函数,而它的声明在对应的h文件中
(2)export专用于模板,类似于extern之于简单类型
(3)实际很多环境不支持,暂不必细究,看到代码时能认出即可
3、requires
(1)C++20引入,用于表示模板的参数约束,因为有些模板函数的参数是有一定限制的,例如add函数其参数不能是两个结构体,结构体是无法相加的
(2)了解即可,暂时不用管
#include
using namespace std;
#define METHOD 0
#if METHOD
//写一个函数add,完成两个数的求和
//若有十种数据类型要考虑,就需要写多个add的重载函数,十分麻烦
int add(int a, in b)
{
cout << "int add(int a, in b)" << endl;
return (a+b);
}
double add(double a, double b)
{
cout << "double add(double a, double b)" << endl;
return (a+b);
}
#else
//如何解决上述问题,可通过模板编程来解决
template <typename T>
T add(T a, T b)//T就被当作这个模板的泛型数据类型
{
return (a+b);
}
#endif
int main(int argc, char *argv[])
{
double i = 5, j = 6;
//short i = 5, j = 6;
//int i = 5, j = 6;
//两个不同的数据类型会将低的转为高的类型(不同数据类型的高低级不同)
cout << "a + b = " << add(i ,j) << endl;
return 0;
}
1、何为异常处理
(1)异常exception,即运行时错误
(2)C中没有异常机制,所以运行时遇到错误只能终止程序
(3)C++中新增了异常处理机制,允许程序在运行时拦截错误并处理,这样程序就不用终止
(4)异常机制的一个典型案例就是:由用户输入2个数字然后相除中的除0异常
2、异常处理编程实践
(1)try(其包含的代码表示有可能爆发错误), catch(捕捉异常), throw(抛出异常)
(2)异常处理机制为什么优于出错条件判断:https://www.cnblogs.com/wkfvawl/p/10816156.html
#include
#define C 0
using namespace std;
int main(int argc, char *argv[])
{
int m, n;
//让用户输入两个数,二者相除返回二者的商
cout << "please input two numbers" << endl;
cin >> m >> n;
#if C //在C中这样处理除数为0的情况
if (n == 0)
{
cout << "n can‘t be 0." << endl;
return -1;
}
else
{
cout << "m / n = " << m/n << endl;
}
#else //在C++中的处理方式
//C++中异常处理机制进行处理
try
{
//try括号里的代码就是有可能触发异常的代码
//这部分代码不应过多,会消耗过多的资源
if (n == 0)
throw(0);//抛出异常
//throw('A');//抛出的这个类型异常无法被抓取,因为下方无catch(char e)
//在执行过程中会出错退出
cout << "m /n = " << m/n << endl;
}
catch(int e)//catch的()里写上要抓取的异常类型
{
cout << "catch int e" << endl;
}
catch(double e)//catch的()里写上要抓取的异常类型
{
cout << "catch double" << endl;
}
#endif
cout << "---other code---" << endl;
return 0;
}
3、异常和函数
(1)在某个函数内throw一个异常后如果没有catch会层层向外传递(顺着函数的调用关系)直到被catch为止
(2)函数可以用throw列表来标识自己会抛出的异常
有时候有些函数只负责抛出异常,有些函数只负责处理异常
void func(void) throw(A, B, C);//这种声明就是告诉调用者可能会抛出A,B,C三种异常
4、标准库中的exception类(标准异常:不是我们自定义的,是标准库定义好的,包括常见的很多错误)
(1)标准库中定义的异常类及其派生类,很多内置代码的错误会抛出这些异常
(2)譬如bad_typeid,使用 typeid 运算符时,如果其操作数是一个多态类的指针,而该指针的值为 NULL,则会拋出此异常
(3)譬如bad_cast,用 dynamic_cast 进行从多态基类对象(或引用)到派生类的引用的强制类型转换时,如果转换是不安全的,则会拋出此异常
5、noexcept关键字
(1)throw(int, double, A, B, C)表示函数可能会抛出这5种类型的exception
(2)throw() 表示函数不会抛出任何异常
(3)C++11中引入noexcept关键字替代throw()表示函数不会抛出任何异常
noexcept(bool)//bool值为true表示无异常
(4)没有throw列表的函数,表示函数可能会抛出任意类型的异常
6、剩余一些关键字
(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)
7、总结
(1)C++关键字和复杂度远超过C语言,语言特性较多
(2)面向对象编程相关特性是C++的基础核心,占比非常大
(3)模板泛型和抽象化编程是C++的重要特征,甚至可以说是精髓所在
(4)和java、python相比,C++的语法细节过多,这也是C++较难学习的重要原因
(5)不要试图去记,以理解为主,配合代码实验去消化吸收,形成自己对C++的认知
(6)经典C++与C++11、14、17、20的差异其实就是相应增加的关键字带来的新语言特性
注:本资料大部分由朱老师物联网大讲堂课程笔记整理而来,如有侵权,联系删除!水平有限,如有错误,欢迎各位在评论区交流。