Coding之路——重新学习C++(1):C++基础知识盲点总结
最近为了找工作参加了许多公司的笔试和面试,发现了以前的知识虽然学了很多,但是并不深入和系统。所以准备把一些书重新读一读,并且打算做一些总结,毕竟老祖宗教导我们“学而时习之,不亦说乎”。
1.把程序分成模块
当我们做程序一般都是分成许多模块去做,因为这样可以保证模块之间的独立性,不会因为一个模块的改动影响整个程序。所以我们在分模块的时候最重要的就是在一系列有关的过程(函数)和它们用到的数据组你开织在一起,在C++中一般放入一个命名空间中或者一个类中。每个模块应该提供接口隐藏数据和功能函数的具体实现,让用户只看到模块的功能使用。简单的说就像开车一样,开车我们只要操纵方向盘,油门等,而把汽车内部具体怎么点火、启动发动机、轮胎旋转的具体过程隐藏了起来。
2.虚函数机制
当子类和父类有类似的功能而不完全相同的时候,我们希望能用父类调用子类的函数,使程序具有更大的灵活性。C++就提供了虚函数机制为我们解决了这个问题。当在类中使用虚函数的时候,每个类都会产生一张表,这张表存着指向这些虚函数的指针,这就是虚函数表(vtbl)。运行时只要调用根据virtual函数名找到vtbl中相关的函数位置,就能调用虚函数。不过类使用虚函数时,每个类的对象都存了一个指针,指针指向的是vtbl。
3.模板是一种编译时机制,也就是说它在编译时就把<typename T>中的“T”替换成具体的类型(int,string等)。与手工编写的代码一样,不会造成运行时的开销。
4.枚举
我们常常用枚举来表示一些常用的常量。枚举仍然还有自己独立的范围,如果枚举中值都是是非负数,该枚举的值就是[0:2k-1],2k是能包括所有枚举值的最小的2的幂。如果存在负数,则是[-2k:2k-1]。例如:enum e1{a = 3, b = 9},它的范围就是0~15。
5.初始化
在初始化的时候,如果没有提供初始化式子,那么静态对象(包括全局的、命名空间的、局部静态的对象)将自动初始化为适当类型的0,而局部对象和在自由存储区里建立的动态对象(通过new、malloc建立的对象)则不会用默认值初始化。
6.字符串常量
在程序中,编译器会把字符串常量当做是“适当个数的const字符类型数组”,像“xsk”就是const char[4]。语法上允许把字符串常量赋值给字符串指针,但是试图通过指针去修改字符串常量是错误的,如果我们希望修改字符串常量,我们需要把字符串复制到char数组中。
char *p = "xsk"; char a[] = "xsk"; p[0] = ‘s‘; //错误,给常量赋值没意义 a[0] = "s"; //正确,a是4个字符的数组
另外,字符串常量是静态分配的,所以可以充当函数返回值。空字符串写成“”,它的类型是const char[1];不过当你把空字符(‘\0’)放入字符串中时,程序只会把空字符当做结尾。例如“xsk\0coding”只会被当做“xsk”。
7.常量
(1)关键字const可以加在一个对象的声明上,使对象成为常量。因为const常量不能赋值,所以必须进行初始化。(const int i =100;)
(2)const常量通常的值都是常量表达式,如果是这样,常量就可以在编译时求值,甚至可以不用为const常量分配内存。
const int c1 = 1; const int c2 = f(3); //编译时不知道c2的值,需要分配空间 extern const int c3; //编译时不知道c3的值,需要分配空间
另外,对于const常量数组是需要分配存储空间的,因为编译器无法弄清楚表达式使用的是常量数组中的那些元素,通常把常量数组放入只读存储器改善效率。
(3)我们可以把一个变量的地址赋值给一个常量的指针,但是我们不能把常量的地址赋值给一个普通指针,这样就允许修改常量了,但是这是错误的。
8.指针和数组
(1)指针的实现是希望能直接映射到程序运行所在机器上的机制。
(2)在C++中,一般空指针用“0”表示,不用“NULL”。
(3)在==应用于指针时,==比较的是地址,而不是指向的对象。
(4)在数组和指针之间,只存在将数组名转换为数组开始元素的指针,反之不能把指针赋值给数组名。
(5)对于void*指针,除了赋值操作和吧void*指针显式转化为其他类型指针外的操作都是不安全的,因为编译器不知道void*指针的具体类型,使用时必须显式转换。函 数指针和成员指针都不能赋值给void*指针。
9.引用
(1)为了保证我们能顺利的使用引用,我们必须对引用进行初始化,并且不能进行更改。引用类似与一个指针,可以同过“&rr”来取得引用的对象地址,但是,引用不是一 个真正的对象不能像操作指针那样操作引用。
(2)对于普通的引用“T&”的初始值必须是一个已经定义的变量,而对于const T&的初始式可以不是一个变量,甚至可以不是一个类型。
double &r = 0; //错误,必须是变量 const double &r2 = 2; //正确。 /* 1.先把2进行double的隐式转换 2.将结果存入一个double的临时变量中 3.将临时变量作为初始值 */
(3)一般函数中都喜欢使用常量引用作为参数。需要区分对变量的引用和对常量的引用,在变量引用时容易引用临时变量,赋值将会是对即将消失的变量的引用,容易出错,并且常量引用作为函数参数容易被函数修改,这通常是不合理的。而对于常量的引用则不会出现这些问题。
10.运算符
(1)对于运算符优先级,[作用域解析符号] > [成员选择符号、类型转换符和value++符号] > [sizeof 、new 、&取地址、++value等一元符号] > [成员选择符号] > [二 元运算符] > [位移符号] > [大于、小于符号] > [等于符号] > [按位逻辑符号] > [逻辑符号] > [赋值符号]。
(2)一个以左值为运算对象的结果仍是左值。不过有时也应该注意下面的情况:
int *q = &(x++); //错误:x++不是左值(值不存储在x中)
11.new 和 delete运算符
(1)在new后,delete时必须知道为对象分配的空间大小,这说明new分配的对象比静态对象占用的空间稍微大一点,通常需要一个机器字保存对象的大小。
(2)operator new 一般不对返回的存储进行初始化,当new无法找到空间分配时默认抛出bad_alloc异常。
12.函数
(1)inline描述符试着给编译器一个提示,把所有的inline func函数实时处理,得出结果。而递归函数则不可能实时处理。
(2)字符常量、常量和需要转换的参数都可以传递给const&,不能传递给非const&。如果将数组作为函数参数,传递的就是数组的首元素指针,类型T[]在作为参数传递时 转化为T*。
(3)每当一个函数被调用时,就会建立所有参数和局部变量的新的副本,函数返回后,这些内存另做他用。所以绝对不能返回临时变量的指针和引用。
(4)函数指针调用函数和初始化时,必须要求参数类型和返回值类型完全一致。
13.名字空间和
namespace Parser{
double prim(bool); double term(bool); using Lexer::get_token; using Lexer::curr_ok; }
(2)以偏向方式解析潜在冲突
namespace His_lib{ class String {/*...*/}; class Vector {/*...*/}; } namespace her_lib{ class Vector {/*...*/}; class String {/*...*/}; } namespace My_lib{ using namespace His_lib; //来自His_lib的所有东西 using namespace Her_lib; //来自Her_lib的所有东西 using His_lib::String; //以偏向His_lib的方式解决冲突 using Her_lib::vector; //以偏向Her_lib的方式解决冲突 class List{/*...*/}; }
(3)我们一般使处理错误的代码与“正常”代码分离。在导致错误的代码的同一抽象层次上处理错误很危险。完成错误处理的代码有可能又产生起错误处理的错误。
郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。