Fork me on GitHub

一些基础的C++豆知识

基础语法

  • 由于C++的枚举不像C#中的枚举,其枚举类型名并不是标识符的一部分,因此经常可能发生命名冲突的问题,解决的方法有四个:在枚举元素名称前加限定前缀(如enum EnumFruit { EnumFruit_apple = 1 };),将枚举类型放在一个同名的命名空间中,或将枚举作为类的嵌套类型,或者使用C++11的enum class(What’s an enum class and why should I care?)。
  • struct和class的默认类继承方式都是private,这与struct的成员默认继承方式是public是不同的。
  • #include_next <filename.h>,include位于搜索路径中位于当前文件之后的文件filename.h。
  • 在vc中,inlucde的路径的反斜杠不需要转义,如#include "..\..\..\Global\Data\GlobalPreferencesMgr.h"
  • 对于namespace中的函数或class的前置声明,必须同样也包括在相同的namespace中,而不能用class ::std::A这种写法。(Why can’t I forward-declare a class in a namespace like this?
  • 没有&&=,只有&=
  • (-1 || 0) == 1,请想想为什么。

位移

  • 在C语言中,涉及位移的运算符有2个,>>表示右移,<<则表示左移。
    而汇编指令中,SHL和SHR表示逻辑左移和逻辑右移,SAR和SAL表示算术左移和算术右移。
    其中,逻辑左移和算术左移都是寄存器二进制位整体向左移动,并在右边补0。
    而右移则不同,逻辑右移是整体向右移,并在左边补0,而算术左移则是根据原符号位的值补与其相同的值。
  • 那么如何在C语言中分别实现逻辑和算术位移呢?根据C标准,如果在位移运算符左边的变量是有符号数,如int,char,short等,编译产生的汇编指令是算术位移指令,如果该变量是无符号数,如unsigned int,unsigned char等,编译产生的汇编指令则是逻辑位移指令。

变量

浮点数

  • 浮点数是不能用 unsigned来规范的。unsigned 的意思就是把内存中的数据第一位也用来表示数据,而不用于表示符号位。而浮点数规定内存中数据的第一位必须是符号位(Double-precision floating-point format)。因此两者之间是互相矛盾的,这也就是为什么浮点数不会有unsigned类型。在某些编译器下unsigned float 和 unsigned double会被自动转换成unsigned int类型,而不报错。这时sizeof(unsigned float)和sizeof(unsigned double)的值是4。
  • 定点数的优点是很简单,大部分运算实现起来和整数一样或者略有变化,但是缺点则是表示范围,而且在表示很小的数的时候,大部分位都是0,精度很差,不能充分运用存储单元。浮点数就是设计来克服这个缺点的,它相当于一个定点数加上一个阶码,阶码表示将这个定点数的小数点移动若干位。由于可以用阶码移动小数点,因此称为浮点数。(为什么叫浮点数?

数组

  • 对于数组char buff[] = “hello”,将buff 和 &buff 用指针形式输出,结果是一样的。(Address of array - difference between having an ampersand and no ampersand
  • How to initialize all members of an array to the same value?
    int array[100] = {0};可以将100个元素都设置成0,但int array[100] = {-1};只能将第一个元素设置成-1,声誉99个元素则设置成0。
  • 不能将一个char[100]的实参传给一个char*&的形参,因为对于引用,必须是类型严格相同的。char[100]跟char*虽然可以相互转换,但编译时类型并不相同。可以将形参改成int (&arr)[100]这样。
  • char * arr[n] = { "aaa", "bbb" }是对的,是char的数组,每一个char指向一个字符串常量。而char ** arr = { "aaa", "bbb" }是语法错误的,这是指向char*数组的二维指针,所以必须先arr = new char*[n]
  • 当数组定义时没有指定大小,当初始化采用列表初始化了,那么数组的大小由初始化时列表元素个数决定。如果明确指定了数组大小,当在初始化时指定的元素个数超过这个大小就会产生错误。如果初始化时指定的的元素个数比数组大小少,剩下的元素都回被初始化为0。字符数组可以方便地采用字符串直接初始化。因此,int a[10] = {0}这种写法,其实本来是将第一个元素置为0,但后续所有元素都会被默认置为0。

new/delete/malloc/free

  • 深入探究C++的new/delete操作符
    new/new[]调用的是operator new/new[],前者是C++关键字,而后者其实就是(全局或class内的)操作符重载。所谓的placement new其实就是operator new/new[]的重载版本,我们也可以自定义提供了更多参数的placement new版本如operator(size_t size, P2, P3, P4),然后通过new(P2, P3, P4)这样的语法进行调用。
  • calloc返回的是一个数组,而malloc返回的是一个对象。calloc的效率一般是比较低的。calloc相当于malloc后再加memset。关于realloc,原来的指针会被Free,申请可能不成功,会返回NULL。新增区域内的初始值则不确定。alloca是在栈(stack)上申请空间,用完马上就释放。某些系统在函数已被调用后不能增加栈帧长度,于是也就不能支持alloca函数。

cookie信息

  • 当我们使用 operator new 为一个自定义类型对象分配内存时,实际上我们得到的内存要比实际对象的内存大一些,这些内存除了要存储对象数据外,还需要记录这片内存的大小,此方法称为 cookie。这一点上的实现依据不同的编译器不同。(例如 MFC 选择在所分配内存的头部存储对象实际数据,而后面的部分存储边界标志和内存大小信息。g++ 则采用在所分配内存的头4个字节存储相关信息,而后面的内存存储对象实际数据。)当我们使用 delete operator 进行内存释放操作时,delete operator 就可以根据这些信息正确的释放指针所指向的内存块。
  • 以上论述的是对于单个对象的内存分配/释放,当我们为数组分配/释放内存时,虽然我们仍然使用 new operator 和 delete operator,但是其内部行为却有不同:new operator 调用了operator new 的数组版的兄弟- operator new[],而后针对每一个数组成员调用构造函数。而 delete operator 先对每一个数组成员调用析构函数,而后调用 operator delete[] 来释放内存。需要注意的是,当我们创建或释放由自定义数据类型所构成的数组时,编译器为了能够标识出在 operator delete[] 中所需释放的内存块的大小,也使用了编译器相关的 cookie 技术。
  • 根据Inside The C++ Object Model上所言,现在的编译器大多使用两种方法, 一种是cookie, 一个记录分配空间大小的内存小块绑定在分配内存的地址头部。二是使用表来对分配了的指针进行管理,每一个分配了空间的指针都在表中对应着分配空间的大小。

指针

  • ANSI规定不能对void指针做++/+=等操作,但GNU将void的这些操作当作和char*一样。(Increment void pointer by one byte? by two?
  • 定义指向public成员函数的指针变量的一般形式为数据类型名 (类名::*指针变量名)(参数表列)。使指针变量指向一个公用成员函数的一般形式为指针变量名=&类名::成员函数名。对于普通函数,函数名本身加不加&都能表示函数指针,但是成员函数则必须加&才能取地址。
  • 在C语言里,一个指针可以指向一个函数。这个指针也有两个属性,但一个是函数的入口地址,另一个是函数的返值类型。但是C里面函数指针的形参列表可以不写出(obsolescent),而C++中则强制要求写出。(Function pointer without arguments types?

函数

  • 默认值可以是全局变量、全局常量,甚至是一个函数。但不可以是局部变量。因为默认参数的调用是在编译时确定的,而局部变量位置与默认值在编译时无法确定。

  • 当一个stack上的数组如char arr[5]作为参数传递给一个函数void func(char* p)或void func(char p[5])时,就降级为一个指针,sizeof只能取到指针本身的大小。要记得sizeof是一个编译器的行为,对于函数而言,有可能被多处调用到,传递来不同大小的数组,因此不可能在编译器完成sizeof。(Why does a C-Array have a wrong sizeof() value when it’s passed to a function?)如果要保留数组类型,则要声明函数为void func(char (&a)[5])。(When a function has a specific-size array parameter, why is it replaced with a pointer?

  • 数组的长度与参数声明无关。因此,下列三个声明是等价的:

    1
    2
    3
    void putValues(int*);
    void putValues(int[]);
    void putValues(int[10]);

数组长度不是参数类型的一部分。函数不知道传递给它的数组的实际长度,编译器也不知道,当编译器对实参类型进行参数类型检查时,并不检查数组的长度。

  • 一个返回void的函数,可以在内部return另一个返回void的函数。
  • 参数默认值可以写在函数声明处,也可以写在函数定义处,但是不能两处同时写,即使两处写的默认值是一样的。但是不同的cpp在声明一个外部函数时,应该可以使用不同的函数参数默认值声明,虽然在同一个cpp中不能看见有两次同一个函数的声明,即使是同样的默认值。这说明,无论是函数的声明还是定义,无论默认值是否相同,同一个函数的默认值定义不能出现两次。
  • string, char*参数都可以用字符串常量作为默认值,说明一个类A的对象,并且支持类型B到A的隐式转换,就可以用B的一个实例b作为参数默认值。(How to set default parameter as class object in c++?
  • 如何定义一个函数指针,指向一个带有默认值参数的函数?
    结论是做不到,只能用类似functor或者std::function等来科里化其中的一个或部分参数值。
  • 普通的函数不需要通过&来取地址,但是成员方法取地址则必须加上&。(If ampersands aren’t needed for function pointers, why does boost::bind require one?

内联函数

  • inline关键字更主要的含义是允许一个函数在不同的编译单元(cpp)同时存在实现,这和在h文件的class定义中直接实现一个方法,而不是将方法的实现放到cpp中,本质上是样的。至于是否会用内联代码代替函数调用,则是由编译器自身决定的。(Difference between implementing a class inside a .h file or in a .cpp file
  • 在cpp中定义inline函数(即使在头文件中再次用inline声明了这个函数),对于其它的cpp而言是没有inline效果的。因为对于编译器而言,每个cpp都是独立的编译单元,因此一个cpp是不能inline另一个cpp中定义实现的inline函数的。(C++ inline member function in .cpp file

构造函数

  • explicit关键字用于取消构造函数的隐式转换,对有多个参数的构造函数使用explicit是个语法错误。即用explict修饰的构造函数有且只能有一个参数。

  • 在构造函数里调用另一个构造函数的关键是让第二个构造函数在第一次分配好的内存上执行,而不是分配新的内存,这个可以用标准库的placement new做到。

      
    1
    2
    3
    4
    A()
    {
    new(this)A(11);
    }

注: 若构造函数调用自身,则会出现无限递归调用。

成员函数隐藏

  • Reason for C++ member function hiding
    当编译器于某一层找到能用(不一定最好,也许需要强制转换参数类型)的方法时,就不会继续再向上一层(父类)查找。不仅仅是类与类之间,嵌套的namespace也存在这个现象。

sizeof

sizeof有三种语法形式,如下:

  1. sizeof( object ); // sizeof( 对象 );
  2. sizeof( type_name ); // sizeof( 类型 );
  3. sizeof object; // sizeof 对象;
  4. size_t sz = sizeof( foo() ); // foo() 的返回值类型为char,所以sz = sizeof( char ),foo()并不会被调用。但是foo不能返回为void。
    c99标准支持对VLA取sizeof。

在VC中规定, 结构体变量的首地址能够被其最宽基本类型成员的大小所整除;而在gcc中规定对齐模数最大只能是4,也就是说,即使结构体中有double类型,对齐模数还是4。

这是因为,C++标准中规定,“no object shall have the same address in memory as any other variable” ,就是任何不同的对象不能拥有相同的内存地址。 如果空类大小为0,若我们声明一个这个类的对象数组,那么数组中的每个对象都拥有了相同的地址,这显然是违背标准的。
基本上所有的指针运算都依赖于sizeof T。

类型转换

  • Why can’t I static_cast between char * and unsigned char *?
    不同的两种类型的指针相互之间不能用static_cast转换,而必须用reinterprete_cast。而普通指针和void*之间则可以用static_cast相互转换。
  • How dynamic_cast works internally?
    Formally, of course, it’s implementation defined, but in practice, there will be an additional pointer in the vtable, which points to a description of the object, probably as a DAG of objects which contain pointers to the various children (derived classes) and information regarding their type (a pointer to a type_info, perhaps).

The compiler then generates code which walks the different paths in the graph until it either finds the targeted type, or has visited all of the nodes. If it finds the targeted type, the node will also contain the necessary information as to how to convert the pointer.

One additional point occurs to me. Even if the generated code finds a match, it may have to continue navigating in order to ensure that it isn’t ambiguous.

typedef

  • typdef定义的struct/class如何前置声明?
1
2
3
4
5
6
7
8
typedef struct my_time_t
{
int hour, minute, second;
} MY_TIME;

struct my_time_t;
typedef struct my_time_t MY_TIME;
void func(MY_TIME* mt) {}

其实typedef作为一种类似宏的声明,在没有include头文件的情况下要想使用只能重新typedef。

  • #define没有作用域的限制,只要是之前预定义过的宏,在以后的程序中都可以使用。而typedef有自己的作用域。(Please explain syntax rules and scope for “typedef”
  • typedef会影响模板参数T的匹配吗?对于func(int, int32_t)和func(int, int),会优先匹配哪个?实验发现编译错误:func重定义。一个int实参不能传给unsigned&的形参,但typedef可以。以上都表明typedef有点类似define。但是,ifdef/ifndef不能检查typedef。
  • typedef register int FAST_COUNTER;,这种写法是错误的,编译通不过。问题出在你不能在声明中有多个存储类关键字(storage class specifier)。因为符号typedef已经占据了存储类关键字的位置, typedef声明中不能用register(或任何其它存储类关键字如static)。此外,由于存储类关键字本身并不是类型type的一部分,因此不允许其出现在typedef语句中也是合理的。(Why typedef can not be used with static?
  • typedef struct tagNode *pNode; struct tagNode { };,在这个例子中,你用typedef给一个还未完全声明的类型起新名字。C语言编译器支持这种做法。
  • typedef struct tagNode { } *pNode;,定义了一种新的类型pNode,等于一个结构体指针类型。
  • typedef char *pStr1; #define pStr2 char *; pStr2 s3, s4; pStr2 s3, s4;,在上述的变量定义中,s1、s2、s3都被定义为char *,而s4则定义成了char,不是我们所预期的指针变量,根本原因就在于#define只是简单的字符串替换而typedef则是为一个类型起新名字。
  • typedef也有一个特别的长处:它符合范围规则(scope),使用typedef定义的变量类型其作用范围限制在所定义的函数或者文件内(取决于此变量定义的位置),而宏定义则没有这种特性。但typedef定义的类型不能用#ifdef 、#ifndef去检测。
  • typedef是一个语句,后面要加分号;。而define是预处理宏,不能加分号。
  • typedef char Line[81];定义了一种新的类型Line,等于char[81],不能错误地写作``typedef char[81] Line;。此外,最好用typedef struct Line { char line[81]; } Line;
  • typedef char * pstr;,定义了一种新的类型pstr,等于char*。按照顺序,const pstr被解释为char * const(一个指向 char 的常量指针),而不是const char *(指向常量 char 的指针)。这个问题很容易解决:typedef const char * cpstr;

cdecl和stdcall

实际上__cdecl__stdcall函数参数都是从右到左入栈,它们的区别在于由谁来清栈,__cdecl由外部调用函数清栈,而__stdcall由被调用函数本身清栈, 显然对于可变参数的函数,函数本身没法知道外部函数调用它时传了多少参数(也许有人说例如printf,分析format string不就可以知道传了哪些参数了,但实际上,caller在调用printf时,可以额外多传一些没有用到的参数啊),所以没法支持被调用函数本身清栈(__stdcall), 所以可变参数只能用__cdecll。
另外还要理解函数参数传递过程中堆栈是如何生长和变化的,从堆栈低地址到高地址,依次存储 被调用函数局部变量,上一函数堆栈桢基址,函数返回地址,参数1, 参数2, 参数3…

STL

  • vector为了防止大量分配连续内存的开销,保持一块默认的尺寸的内存,clear只是清数据了,未清内存,因为vector的capacity容量未变化,系统维护一个的默认值。有什么方法可以释放掉vector中占用的全部内存呢?(vector< T > vtTemp; veTemp.swap( vt );)
  • multimap/multiset不支持下标运算。
  • const map不支持下标操作,[ ]没有提供const版本。由于[ ]对于不存在的key,会自动insert相应的key,所以const map版本没有提供[ ]。

longjmp

在C++中用longjmp,可能导致析构函数不被调用。


本文地址:http://xnerv.wang/cpp-bean-knowledge/