工作中发现自己对C++掌握的不够扎实,于是利用业余时间系统的复习下C++,看完C++primer之后又买了唐佐林老师的视频,看视频的过程中做了如下笔记。
第一遍过程中需要复习的内容
第一课:学习C++的意义
第二课,C到C++的升级,不需要复习,只需要记住中间的三个知识点;
C++更强调语言的实用性,所有的变量都可以在需要使用时再定义,(比较典型的例子就是for循环的控制变量可以直接在for后面的括号里面定义),而C语言中的变量都必须在作用域开始的位置定义。
C语言中struct关键字只是定义了一组变量的集合,并没有定义一种新的类型,而C++中的struct定义了一个全新的类型,
C++中的
struct Student
{
const char *name;
int age;
}
等价于C语言中的
typedef struct _tag_student Student;
struct _tag_student
{
const char* name;
int age;
}
C++中所有的标识符都必须显式的声明类型。
在C语言中,int f()表示返回值为int,接受任意参数的函数,f(void)表示返回值为int的无参函数
在C++中,int f()和int f(void)具有相同的意义,表示返回值为int的无参函数。
第三课,进化后的const分析
C语言中,const修饰的变量不是真的常量,本质还是变量,const只是使变量具有只读属性,也就是C语言中const不能定义真正意义上的常量。c语言真正意义上的常量只有enum枚举类型。
C语言中,const只是在编译期有用,在运行期无用,他只是告诉编译器该变量不能出现在赋值符号的左边。
C语言中,const修饰的局部变量在栈上分配空间,const修饰的全局变量在只读存储区分配空间。
#include<stdio.h>
int main()
{
const int c = 0;
int * p = (int*)&c;
*p = 5;
printf("c = %d ", c);
printf("*p = %d ", *p);
return 0 ;
}
以上程序,用GCC编译之后,c的值打印出来为5,*p的值打印出来也是5,
用G++编译之后,c的值打印出来还是0,*p的值打印出来是5.
在C++语言中,当碰到const声明的常量时,会把常量的值放到符号表中,后面编译过程中如果遇到使用常量的地方,直接用符号表中的值替换常量,这样在C++中const修饰的就真正是常量了。
但是C++为了兼容C语言,C++在下述两种情况下会给对应的常量分配存储空间,(1)对const常量使用了extern,(2)对const常量使用&操作符,但是C++编译器在这两种情况下虽然为const常量分配了空间,但是它并不会使用其存储空间的值。这么做就是为了兼容C语言。
符号表就是编译器在编译过程当中所产生的一张表,这张表是编译器内部的数据结构,上面的程序中,执行到const int c = 0时,编译器会把c保存到符号表中,然后执行到第二行int * p = (int*)&c;这个时候编译器会给c分配内存空间,但是不使用空间中的值。
C++中的const常量有点类似于宏定义,例如const int c = 5类似于#define c 5;但是还是有区别的,
const常量是由编译器处理的,编译器对const常量进行类型检查和作用域检查。
宏定义是由预处理器处理的,单纯的文本替换,编译器根本就没有宏的概念,宏替换之后就是一个字面量,宏没有类型也没有作用域。
#include<stdio.h>
void f()
{
#define a 3
const int b = ;
}
void g()
{
printf("a = %d ", a);
//printf("b = %d ", b);
}
int main()
{
const int A = 1;
const int B = 2;
int array[A + B] = {0};
int i = 0;
for(i = 0; i < (A + B); I++)
{
printf("array[%d] = %d ", i, array[i]);
}
f();
g();
return 0;
}
用GCC编译的时候,int array[ A + B]报错,在C语言中A + B是两个变量相加,两个变量相加需要在运行过程中才能得到, 这样编译器也就不知道这个数组的大小,
用G++编译的时候,此行不报错,因为在C++中A + B是两个常量,
另外,g()中打印a的值时,无论GCC还是G++都没有报错,在g函数中访问f函数中定义的宏竟然编译通过,因为宏是被预处理器处理的直接进行文本替换,编译器根本就不知道宏是什么,编译器在g函数看到的那行打印是printf("a = %d ", 3);也就是宏是没有作用域的概念的, g函数中如果把打印b的那一行放开,那么会报错,因为编译器会对b进行类型和作用域的检查。
第四课,布尔类型和引用
C++在C语言的基本类型系统上增加了bool,C++中的bool可取的值只有true和false,bool只占用一个字节。
C++编译器会将非零值转换为true,0值转换为false。
C语言中的条件表达式返回的是变量值,不能作为左值使用,C++中的条件表达式可直接返回变量本身,既可以作为左值使用,又可以作为右值使用,但是条件表达式中可能返回的值如果有一个是常量,则不能作为左值使用。
什么是变量,变量是一段连续存储空间的别名,程序中通过变量来申请并命名存储空间,通过变量的名字可以使用存储空间。
引用即别名,普通引用在定义时必须初始化。
实际上,当三目运算符的可能返回值都是变量时,返回的是变量引用,当三目运算符的可能返回中有常量时,返回的是值。int a = 1;int b = 2;
(a < b ? a : b) = 3;//正确,返回a或b的引用,可以作为左值。
(a < b ? 1 : b) = 3;//错误,返回1或b的值,不能作为左值。
第五课,引用的本质分析
引用作为变量别名而存在,因此一些场合可以代替指针。
const引用让引用变量拥有只读属性,int a = 4; const int & b = a; int *p=(int*)&b; b = 5; //error,只读变量, *p=5;//ok,修改变量a的值。 在C++中,如果我们想让一个已经存在的变量变为只读变量,只需要定义一个const引用就可以了。
引用只能是另一个变量的别名,引用不可能是一个常量值的别名,因此不能用字面常量对引用进行初始化。但是const引用可以,
我们可以用一个字面值常量对const引用进行初始化,当使用常量对const引用进行初始化时,C++编译器会为常量值分配空间,并将引用名作为这段空间的别名,也就是使用常量对cosnt引用初始化后将生成一个只读变量。
const int &b = 1;//ok int * p = (int *)&b; b = 5;//error,只读变量, *p=5;//ok,我们可以用指针改变这个只读变量的值,
引用也是有自己的存储空间的,引用在C++中的内部实现是一个指针常量,Type &name; ===Type * const name;
1.C++编译器在编译过程中用 指针常量 作为引用的内部实现,因此引用所占用的空间大小与指针相同。2,从使用的角度,引用只是一个别名,C++为了实用性而隐藏了引用的存储空间这一细节。
C++中的引用旨在大多数的情况下代替指针,功能性:他可以满足多数需要使用指针的场合,安全性:可以避开由于指针操作不当而带来的内存错误。操作性:简单易用。
第六课,内联函数分析
宏代码片段由预处理器处理,进行简单的文本替换,没有任何编译过程,因此会出现副作用,C++中推荐使用内联函数替代宏代码片段,C++中使用inline关键字声明内联函数,内联函数声明时inline关键字必须和函数定义结合在一起,否则编译器会直接忽略内联请求。
内联函数没有普通函数调用时的开销,另外,inline只是向编译器请求,编译器可能会拒绝函数的内联请求。
第七课 函数参数的扩展
C++可以在函数声明时为参数提供一个默认值,当函数调用时如果没有提供参数的值,则使用默认值。
参数的默认值要从右到左提供,函数调用时使用了默认值,则后续参数必须使用默认值。
C++中可以为函数提供占位参数,占位参数只有参数类型声明,没有参数名字声明,一般情况下,在函数体内部没法使用占位参数。
int func(int x, int){return x;} func(1, 2);
那么既然函数体内部并不能使用占位参数,那么C++为什么要这么做呢,
C++语言用占位参数特性是为了兼容C语言中可能出现的不规则的写法,例如在C语言中void func()和void func(void)这两个函数声明是不等价的,前者表示可以接受任何参数,后者表示不接受参数,而在C++中两个函数声明时等价的。这样有些C代码移植到C++中是会编译出错的,而我们利用C++的占位参数特性能够经过很小的修改就可以让C代码在C++中编译通过。
第八课函数重载分析(上)
函数重载:用同一个函数名定义不同的函数。函数重载至少满足下面三个的一个条件:参数个数不同,参数类型不同,参数顺序不同。但是函数返回值不能作为函数重载的依据。
当函数默认参数遇上函数重载,如果出现二义性,则编译不通过。
int func(int a, int b, int c = 0)
{
return a * b * c;
}
int func(int a, int b)
{
return a + b;
}
int main()
{
int c = func(1, 2);//存在二义性
return 0;
}
函数重载在本质上是相互独立的不同函数,重载函数的函数类型不同。
第九课函数重载分析(下)
将重载函数名赋值给函数指针时,1.根据重载规则挑选与函数指针参数列表一致的候选者,2.严格匹配候选者的函数类型与函数指针的函数类型。
重载与指针:注意事项:函数重载必须发生在同一作用域中,C++中无法直接通过函数名得到重载函数的入口地址,
int add(int a, int b)
{
return a + b;
}
int add(int a, int b, int c)
{
return a + b + c;
}
int main()
{
printf("%p ", (int (*)(int ,int))add);//这里必须把add进行强制类型转换,否则编译报错。
return 0;
}
C++和C相互调用:用extern "C"{}声明,使C++编译器按照C语言语法进行编译, 但是extern "C"声明是C++中的语句,在C语言中使用此声明编译报错,因此我们用宏__cplusplus来判断当前的编译器是C编译器还是C++编译器,__cplusplus宏是C++编译器内置的宏,下面的语句就是我们在C和C++混合编程中经常见到的。
#ifdef __cplusplus
extern "C" {
#endif
.........其他代码...
#ifdef __cplusplus
}
#endif
C++编译方式将函数名和参数列表编译成目标名,C编译方式只将函数名作为目标名进行编译。
第10课 C++中的新成员
new关键字是C++的一部分,而malloc是C库提供的函数,如果你所使用的C编译器没有提供库函数,那么意味着你所使用的C编译器没法使用动态内存分配。
new以具体类型为单位进行内存分配,而malloc以字节为单位进行内存分配。
new在申请单个类型变量时可进行初始化,而malloc不具备内存初始化的特性。例如int * pi = new int(1);申请内存的同时初始化为1. 注意int * pa = new int[1];这一行申请的是一个数组,
C++中的命名空间:
在C语言中全局作用域只有一个,所有的全局标识符共享此作用域,标识符之间可能会发生冲突。
命名空间将全局作用域分成不同的部分, 不同命名空间中的标识符可以同名而不会发生冲突。
第11课 新型的类型转换
C语言中的强制类型转换方式太粗暴,任意类型之间都可以进行转换,编译器很难判断其正确性,例如
#include <stdio.h>
typedef void(PF)(int);
struct Point
{
int x;
int y;
};
int main()
{
int v = 0x12345;
PF* pf = (PF*)v;
char c = char(v);
Point* p = (Point*)v;
pf(5);
printf("p->x = %d
", p->x);
printf("p->y = %d
", p->y);
return 0;
}
上面这段代码编译是可以通过的,但是运行会出现段错误。
C++将强制类型转换分为四种不同的类型:static_cast const_cast dynamic_cast reinterpret_cast 用法xxx_cast<Type>(Expression)
static_cast主要用于基本类型的转换,例如int double float,不能用于基本类型指针之间的转换,可以用于有继承关系类对象之间的转换和类指针之间的转换。
const_cast 用于去除变量的只读属性,强制转换的目标类型必须是指针或引用。
reinterpret_cast 用于指针类型间的强制转换(reinterpret是重解释,就是将一个指针类型重新解释为另一个指针类型),另外在嵌入式开发的时候有可能会把一个整数强制类型转换为一个指针,这个时候也用reinterpret_cast。
dynamic_cast:只能用于指针之间的转换,并且是类对象之间的指针(用于有继承关系的类指针间的转换,用于有交叉关系的类指针间的转换),具有类型转换功能(也就是说如果进行了强制类型转换,这个转换不成功的话, 那么danamic_cast得到的是空指针,),需要虚函数的支持(就是我们所定义的类里面必须有虚函数才行,)
例程:
#include <stdio.h>
void static_cast_demo()
{
int i = 0x12345;
char c = 'c';
int* pi = &i;
char* pc = &c;
c = static_cast<char>(i);
pc = static_cast<char*>(pi);//error,因为static_cast不能用于基本类型指针的转换
}
void const_cast_demo()
{
const int& j = 1;//定义了只读变量
int& k = const_cast<int&>(j);//j的只读属性被去掉,去掉之后j这个只读变量就变成了一个只读变量,然后用k就可以访问j并且修改它,
const int x = 2;//定义了真正意义上的常量,
int& y = const_cast<int&>(x);//真正意义上的常量是保存在符号表里面的,是去不掉常量属性的,但是我们的编译器还是会为真正意义上的常量分配空间,y就是这个空间的别名,
int z = const_cast<int>(x);//error,const_cast只能用于指针或引用之间的强制类型转换
k = 5;
printf("k = %d
", k);//5
printf("j = %d
", j);//5
y = 8;
printf("x = %d
", x);//2
printf("y = %d
", y);//8
printf("&x = %p
", &x);//0xbfd76330
printf("&y = %p
", &y);//0xbfd76330
}
void reinterpret_cast_demo()
{
int i = 0;
char c = 'c';
int* pi = &i;
char* pc = &c;
pc = reinterpret_cast<char*>(pi);//ok
pi = reinterpret_cast<int*>(pc);//ok
pi = reinterpret_cast<int*>(i);//ok
c = reinterpret_cast<char>(i); //error
}
void dynamic_cast_demo()
{
int i = 0;
int* pi = &i;
char* pc = dynamic_cast<char*>(pi);//error
}
int main()
{
static_cast_demo();
const_cast_demo();
reinterpret_cast_demo();
dynamic_cast_demo();
return 0;
}
第12课 经典问题解析
const什么时候是只读变量,什么时候是常量,:
只有用字面值初始化的const常量才会进入符号表,
使用其他变量初始化的const常量仍然是只读变量。
被volatile修饰的const常量不会进入符号表。volatile表示我们声明的标识符是易变的,在当前文件外部例如多线程,中断等会使该变量发生改变,我们每次访问volatile所修饰的标识符时应该到内存里面去直接读取,也就意味着被volatile修饰的标识符不可能进入符号表,也就是当volatile和const同时修饰一个标识符的时候,得到的仅仅是一个只读变量,不可能进入符号表,此时const的意义仅仅是说明在当前的文件当中或者说当前的作用域当中,我们用volatile和const一起修饰的标识符不能出现在赋值符号的左边。
归纳起来为:在编译期间不能直接确定初始值的const标识符,都被作为只读变量处理。
const引用的类型与初始化变量的类型, 相同:初始化变量成为只读变量, 不同:生成一个新的只读变量。