God's in his heaven.
All's right with the world.

0%

一些基础的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等,编译产生的汇编指令则是逻辑位移指令。
  • 虽然intel平台上都是little-endian字节序,如果看作左边是低端地址右边是高端地址,则左移似乎丢弃低端bits。其实,对于C语言,在移位逻辑上要看作是big-endian,例如0x1001,左边是高位,左移是丢弃高位bits。

变量

"test"是字面常量,和global或static变量具有类似的生命周期。

变量修饰关键字

  • const是限制指针还是限制指向的变量,关键看const是在星号*的左边还是右边。。如const int *cptrint const *cptr是限制int,说明指向的是一个常亮。int *const cptr是一个常量指针,不能再指向其它int变量。(更简单直观的看法是,看const的右边是什么,*cptr是原变量本身,而cptr是指针)
  • const int *指针不能赋值给int *指针,因为一个是指向const int类型,一个是指向int类型。而int * const指针可以赋值给int *指针,因为两者都是指向int变量,指向同类型变量的指针之间的相互赋值,是不受指针本身是否为const的影响的。
  • 对于func(const char*),正如上面一条所说的,可以将char*实参传递过来。但是对于fun(const char*& p)这种加了引用的函数,不能将char*的指针传给它,而必须传const char*指针,因为引用必须引用相同的类型,一个const char*的引用不能去引用一个char*的变量。
  • auto这个关键字用于声明变量的生存期为自动,即将不在任何类、结构、枚举、联合和函数中定义的变量视为全局变量,而在函数中定义的变量视为局部变量。它是存储类型标识符,表明变量(自动)具有本地范围。块范围的变量声明(如for循环体内的变量声明)默认为auto存储类型。
  • mutable关键是为了针对const而提出来的关键字。extern关键字则是针对static(C语言)提出来的关键字。register关键字是针对volatile提出来的关键字。volatile与const一样需要弄清楚修饰的是变量本身还是指针,以及哪一级的指针
  • const int &a =100是正确的,但去掉const就是错误的。

浮点数

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

数组

  • 对于数组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);
    }

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

  • 成员初始化列表不能对基类成员变量赋值,而应该通过调用基类的构造函数达成目标。
  • C++初始化类成员时,是按照声明的顺序初始化的,而不是按照出现在初始化列表中的顺序。(Order of execution in constructor initialization list
  • 删除一个强转成void*的对象指针,会释放内存,但不会调用其析构函数。Is it safe to delete a void pointer?
  • 不要在有虚表的类的构造函数和析构函数中调用虚函数,会调用到基类的函数。([Calling virtual functions inside constructors(https://stackoverflow.com/questions/962132/calling-virtual-functions-inside-constructors)],https://stackoverflow.com/questions/962132/calling-virtual-functions-inside-constructors
  • What is a non-trivial constructor in C++?
    也就是说,trivial构造函数即没有定义构造函数。但没有自定义任何构造函数(包括拷贝构造函数)时,应该会由编译器自动生成trivial构造函数(其实就是什么都不做,进行对象的拷贝赋值时,直接进行memory copy)。如果只定义了一个带参数的构造函数的话,则不会再生成默认的构造函数。

    If you define a constructor yourself, it is considered non-trivial, even if it doesn’t do anything, so a trivial constructor must be implicitly defined by the compiler.
    For a default constructor and destructor being “trivial” means literally “do nothing at all”. For copy-constructor and copy-assignment operator, being “trivial” means literally “be equivalent to simple raw memory copying” (like copy with memcpy).

成员函数隐藏

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

哑元函数

  • C++的哑元参数是指operator ++(int)这种。某个参数如果在子程序或函数中没有用到,那就被称为哑元。函数的形参又称“哑元”,实参又称“实元”。
  • 。友元关系不能被继承。(Why does C++ not allow inherited friendship?
  • 至少在gcc里,int a; string b; 1 == 1 ? a : b;这种写法是可以的,但如果将这个表达式进行cout,就会编译报错提示两边类型不一直的。(Return type of ‘?:’ (ternary conditional operator)

库函数

  • memmovememcpy的区别,在于前者当src < dest并且两者区间有重叠时,会改用从后向前复制。memmove函数为什么要先判断重叠,而不是直接从尾部向头部复制?因为当dst的头部和src的尾部覆盖时,从尾部开始复制是正确的。但是当dst的尾部和src的头部覆盖时,从头部开始复制才是正确的。所以要区分处理。
  • memccpy用来拷贝src所指的内存内容前n个字节到dest所指的地址上。与memcpy不同的是,memccpy如果在src中遇到某个特定值(int c)立即停止复制。
  • strtok是一个线程不安全的函数,因为它使用了静态分配的空间来存储被分割的字符串位置(C库还有其它使用了静态空间的线程不安全函数)。运用strtok来判断ip或者mac的时候务必要先用其他的方法判断’.‘或’:'的个数,因为用strtok截断的话,比如:"192…168.0…8…"这个字符串,strtok只会截取四次,中间的…无论多少都会被当作一个key。而这个函数的线程安全版本在linux中是strtok_r,在vc中则是strtok_s
  • sprintf和vsprintf的区别,以及snprintf和vsnprintf的区别,在于后者接收的是va_list,而前者是不变参数列表,后者几乎不会被直接使用,而是在不定参数的函数内部调用,作为一种“转发”。
  • 在C++中用longjmp,可能导致析构函数不被调用。
  • std::bind是基于functor仿函数实现的,也就是函数对象实现存储固定的参数值。

C++的4种cast操作

  • static_cast/dynamic_cast/reinterpret_cast不能将一个const T*转为T*,当然如果是将const T转化T是可以的。只有const_cast能将const T*转为T*

  • reinterpret_cast转换后的bits是不变的,因此在将double=1.0转变为int时,显然reinterpret_cast会得到诡异的结果。

  • rtti包括typeid(type_info)dynamic_cast两者,都需要虚表的支持。如果是没有虚函数的类,则dynamic_cast就只能从下往上安全转换了。此外,dynamic_cast只能对指针(引用)操作。static_cast是C++里面的类型安全转换,这个转换不允许将毫无关系的两个数据类型的指针互相转化。例如不能把int**转成void**,因为int*void*没有关系,但是可以将int*转成void*

  • 综上所述,C风格的强制转换=static_cast + reinterpret_cast。对于有虚表的类的指针,reinterpret_cast由于不会调整this指针,也不会将vptr指针进行上溯或下溯,因此将会造成不可预期的结果。

  • static_cast可以将void*转换为A*,但是不能量B*转换为A*

  • bad_cast这个关键字和bad_typeid类似,是dynamic_cast转换失败时(一般是错误地想把基类转换为子类时,此时转换结果为空指针),会抛出的异常。

  • const_cast:允许添加或删除表达式类型的constvolatile关键字.

  • dynamic_cast:仅适用于多态类型的向下转换,被转换的类型必须是一个指向含有虚函数的类类型的指针,否则会编译错误。

  • reinterpret_cast:从位的角度来看待一个对象,从而允许将一个东西看成是完全不同的另一个东西,最强的一种转换。这个操作符能够在非相关的类型之间转换。操作结果只是简单的从一个指针到别的指针的值的二进制拷贝。在类型之间指向的内容不做任何类型的检查和转换。例如将一个double转化为int,reinterpret_cast仅仅复制bits,导致转化的值无意义。而static_cast就能得到正确的退一法值。

  • 只有dynamic_cast是运行期行为,其它三种cast都是编译器行为。

  • How is dynamic_cast implemented
    dynamic_cast can know this by keeping this knowledge around.
    When the compiler generates code it keeps around the data about the class hierarchies in some sort of table that dynamic_cast can look up later. That table can be attached to the vtable pointer for easy lookup by the dynamic_cast implementation. The data neeeded for typeid for those classes can also be stored along with those.

  • 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.

  • dynamic_cast失败时,有时是返回null,有时是抛出异常。原因在于C++没有null reference,所以只能throw exception。

    1
    2
    3
    Base* b1 = new Derived;
    Derived* pd1 = dynamic_cast<Derived *>(b1); // fails: returns 'NULL'
    Derived d1 = dynamic_cast<Derived &*>(b1); // fails: exception thrown

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。

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…

多态、继承

  • C++的派生类在重写virtual函数时,访问修饰符可以和基类不同,但是要注意派生类中对基类方法的重载将会导致罕见的“隐藏”问题无论这个函数是不是虚函数
  • 重载方法(包括运算符重载)时是可以改变返回值类型的,因为返回值类型不是函数签名的一部分。
  • 当有虚函数时,应该把析构函数声明为虚析构函数,否则通过基类指针释放派生类对象时,有可能会存在内存泄漏(object的空间本身应该无论如何是可以释放掉的,只是基类的析构函数由于没有被调用,可能会泄露基类对象本身拥有的一些其它内存或资源)。
  • 三种继承方式下基类的私有成员对派生类都不可见,而公共成员和保护成员对派生类的方法而言都可以访问。三者的区别是,公共继承时基类的公共成员和保护成员对派生类而言仍然是公共成员和保护成员,私有继承时基类的公共成员和保护成员都成为派生类的私有成员,保护继承时基类的公共成员和保护成员都成为派生类的保护成员(而对于外界,无论是哪种继承方式,保护成员和私有成员都是不可见的)。
  • 在protected和private继承时,基类指针不能指向派生类对象。简单的说这两种继承方式并不是所谓的is-a关系。详细一点讲,用了这两种继承方式后,子类对象中的继承方法都是在main中不能访问的,如果允许基类指针指向子类对象,就会出错了。当然你也可以用(Base*)进行强制转化。
  • C++的默认继承方式是private继承。

模板

函数模板的偏特化

严格的来说,函数模板并不支持偏特化,但由于可以对函数进行重载,所以可以达到类似于类模板偏特化的效果。
template <class T> void f(T); (a)
根据重载规则,对(a)进行重载
template < class T> void f(T*); (b)
如果将(a)称为基模板,那么(b)称为对基模板(a)的重载,而非对(a)的偏特化。C++的标准委员会仍在对下一个版本中是否允许函数模板的偏特化进行讨论。

C++ traits

C++ traits是利用模板特化编译器来完成一定功能的技巧,本质是“利用类型固有的特性,判断类型是否具有特定的特性”,例如简单的例子(利用偏特化):

1
2
3
4
5
6
7
template <typename T>
struct is_void
{ static const bool value = false; }

template <>
struct is_void<void>
{ static const bool value = true; }

STL

  • C++语言中的std::remove(vec.begin(), vec.end(), 5);并非是删除容器里变所有值等于5的数,而是用类似LeetCode中的27. Remove Element的算法,将后面的元素向前复制移动。因此vector的长度并不会改变,需要和erase方法结合使用:vec.erase(std::remove(vec.begin(), vec.end(), 5), vec.end());
  • vector为了防止大量分配连续内存的开销,保持一块默认的尺寸的内存,clear只是清数据了,未清内存,因为vector的capacity容量未变化,系统维护一个的默认值。有什么方法可以释放掉vector中占用的全部内存呢?根据StackOverflow上的方法,可以用vector< T > vtTemp; veTemp.swap(vt);
  • multimap/multiset不支持下标运算,可能是因为[]运算符可能有多个元素匹配
  • const map不支持下标操作,根据StackOverflow的说法,[]运算符在key不存在时会插入新的元素,不符合const的语境。
  • stl的deque,queue,stack,heap:deque和vector、list一样是一种基础数据结构。然后stack,queue,priority_queue则是可以使用某个基础数据机构作为底层存储的二级数据结构。而heap本身不能持有数据存储,只能将某个基础数据结构对象作为托管的数据存储。
  • STL的模板参数T类型也可以带const修饰符。
  • STL中有bitset这种数据结构。

编译器优化

That is called Named Return Value Optimization and copy elision, and basically means that the compiler has figured out that the copy can be avoided by carefully placing the temporary and the object in the same memory location.
By default there would be three objects in that piece of code, temp inside fun, the return value and ob inside main, and as many as two copies, but by carefully placing temp in the same memory location as the returned object inside fun and placing ob in the same memory address the two copies can be optimized away.
Ref to Value semantics: NRVO, and Value semantics: Copy elision.


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