看C++Primer总结

最近看了C++Primer 这本书籍,觉得写得非常不错。它基本上包揽了C++中的大部分语言特性。该书主要分为基本语言特性(低级抽象级)、类特性(高级抽象级)、模板特性(类型抽象级)、标准库、其他高级工具(命名空间、异常机制,运行时类型信息、内存管理)。现对这几个大块进行一下总结,也是对自己所学的知识一个回顾。

一、基本语言特性

这一块基本上与其他高级语言差不多,也基本上继承了C语言中的大部份特性。也就是围绕了C++中的内置类型(char、int、short、long、unsigned、float、double),以及复合类型(数组、指针、枚举), 表达式的组成(其中包含了内置运算符的含义以及优先级、结合性等),关于复合表达式中的求值顺序这是很多程序员忽略的一个重要特性,因为这个涉及了程序的可移植性,当出问题时也有可能一下子找不到原因。比如: fun(I) + fun(++I); 这个表达式的结果值可能在不同的机器上运行的结果是不一样的。因为fun(I) 与 fun(++I) 这两个函数的执行顺序不一样的话,得出的结果就会不一样。C++中的语句跟其他语言也差不多,主要是分为了控制语句(无非就是顺序语句,选择语句、循环语句等组成。)、赋值语句、函数调用、表达式语句、空语句、复合语句等。其实C++语句最大特别';'是语句的组成部分,每个表达式只要后面加个';'就可以组成一个C++语句了。 还有像在for语句中可以在for语句头中定义变量,如:for(int a = 0, b = 100; a < b ; a++);  还有就是函数的定义,C++中多了一个内联函数特性,主要为了减轻函数调用带来的系统开销。其中也包含了函数重载,默认参数等特性。以下我就这些特性中有个关注点做一个总结:

1、int型认识:像这个类型是与机器相关的,在不同的机器上所占用的大小是不一样的,所以为了代码的移植性,在必须保证其大小的地方最好不要直接使用int型,也就是int类型可能不能容纳其大小。所以一般性C运行时库,c++标准库中都有一套自己的类型,目的就是为了这些库在不同平台上的可移植性。所以我们在使用运行库时最好要使用里面的类型,而不是用C++的内置类型,比如像size_t就是C运行库中的一个类型,用sizeof返回的值都是存在这个类型中。因为size_t类型保证了足够大小来容纳,不管在什么平台上。

2、关于运行库中有些函数参数使用了size_t类型,有些函数是使用unsigned int 类型认识:这都是C标准库定义的行为,比如_memccpy函数与memcpy函数基本上完成的功能差不多,但前面一个是用了unsigned int 类型,而后面这个是用了size_t类型的,其实这些都是C标准库规定的,它规定_memccpy函数拷贝的数据大小用unsigned int来指定就可以了,因为它本身拷贝的就是小批量字符数,不管在16为机器上还是在32机器上unsigned int 都足够容纳了。所以在调用这个函数时,必须注意了,unsigned int因为与机器相关,所以在调用时值不能大于它本身定义的最小范围的值。如果想拷贝大量数据,就必须用memcpy函数,因为这个函数是用的是size_t类型,该类型保证能容纳足够大小。所以我们在编写程序时,如果定义某变量,要保证在不同的机器上能足够容纳大小,则要使用size_t类型,而不是直接使用int型。

3、char 类型认识:这个类型其实也是跟机器相关的,在不同的编译系统上它所代表的意义可能不同,有可能有些平台上是有符号类型,在有些平台上是无符号类型,所以为了代码的可移植性,最好不要将char类型进行运算,如果要参与运算最好明确指出unsigned char 还是 singend char。也就是当表示为字符型时或者一块字节缓冲时,可以用char,因为不管无符号还是有符号,都能容纳下所有字符或者代表单个字节。但如果是表示单字节整数值,最好要明确表示是无符号还是有符号的。还有就是对于宽字符常量,可以通过L来指明,比如L“abc”; 这个字符串就是用UNICODE来编码的。

4、关于数组与指针认识:在C++中 数组名本身就是代表的一个地址,是一个常数,而不是代表整个数组变量, 所以不能给数组与数组直接赋值。而且他代表的是往里推一层的指针类型,比如 int arr[10]; 则arr表明是int* 指针类型,int arr[10][10]; 则arr表明是 int (*)[10] 这个指针类型。数组与指针是紧密关联在一起的,在操作数组时,不管用下标操作符还是*操作符都是使用的是地址计算。还有就是&、*这两个操作符在处理数组时,还有特殊用法。比如像 int arr[10]; 如果对&arr这样操作,不是对arr数组取地址,因为arr本身就是代表数组的首地址,而这个运算只是表明了地址的表示方法,&arr代表是一个行地址,而不是一个列地址了,也就是说&arr代表的是一个int(*)[10]这个类型地址了。同样int arr[10][10],  本身arr代表是一个行地址,如果对*arr这样元素,不是指向某个元素,可以取值了。而是改变了这个地址的含义了,*arr变成了列地址了,也就是一个int* 型指针了。

5、关于常引用的认识:在C++中一个特有类型就是引用类型,它给语言带来了安全性与稳定性。这里有比较特殊的就是一个常引用可以关联一个常数,或者表达式等。比如:const &int a = 100; 这样也是合法的。其实编译系统会给这个100常数分配一个临时变量,把a关联与此临时变量。所以对a的读取就是对那个临时变量内容的读取。

6、关于常量类型的认识:在C++中定义常量要比其他语言灵活的对,const 定义不一定要一个直接常数,它只是表示这是一个常量,只能读取,不能修改而以。也是说在栈、堆、静态区都可以标识一块常量区,以保护它只能读而不能写。比如const int a = b ; 如果这个在函数体中定义,这表明 a 在堆栈中是常量,不能修改。如果在文件作用域中定义,则表明在静态区是一个常量。这都表明a是占内存空间的,而不是一个直接常数。所以一般在头文件中不要这样定义,因为在每个CPP文件中包含它,都会各自分配内存的。至于它们互不干扰,是因为const定义默认是static属性的,所以如果是一个占内存的const定义,最好在一个源文件中这样定义extern const int a = b; 在头文件中这样声明extern const int a; 要在其他地方使用这个常量时只要包含这个头文件就可以了。

7、C++中硬性规定的三个求值顺序的操作符:&& || ?: , 这几个操作符规定了其求值顺序,在代码中存在可移植性。像&& || 规定了先计算左边的,而像 ?:是先计算条件表达式的,逗号表达式就是从左到右开始计算的。

8、C++类型转换认识:C++中还是分为隐式转换与强制转换两种。无非引入了static_cast、const_cast、dynamic_cast、reinterpret_cast这几个转换操作符用于强制类型转换,目的是增强了语言的安全性。static_cast这个操作符主要是用于对于编译系统本身可以隐式转换的地方用于static_cast这个操作符,以表明自己的意图。而const_cast这个转换是对常量去掉了const属性,这个一般性不太会用,如果用了也表明程序设计有问题。dynamic_cast主要是用于运行时动态转换的,这个与delphi中 AS 操作符很相似,如果转换出错就返回NULL或者发生异常。比如将一个指向基类的指针转换为指向派生类的指针时,如果转换出错就返回NULL,如果将一个引用基类的变量转换为引用派生生类的变量,如果转换出错就发生异常,这个操作符是需要运行时类型信息机制的支持(后续会讲到这个机制)。reinterpret_cast这个转换符就是与C语言中的老式强制转换符一样的,是最低级的转换,一般性这个转换是最不安全的,但是是最灵活的。一般性在特殊需要地方这个转换符最为有用。

9、int()语法认识:我认为这是创建临时对象的特殊形式,一般性类类型()就是创建这个类的一个临时对象,而对于int()编译器会自动转换为直接数常量0.比如 int a = int(); 就是a的初始值为0.这种语法的支持就是为了泛型编程提供便利。

10、switch语句中定义变量认识: 在switch语句中,不能直接在case语句的整个块中定义变量,因为case语句只是起到一个标号作用,只要匹配了,代码就一直往下执行,不会再去匹配其他标号了。如果这样导致下面标号的块能访问上面标号中定义的变量,这样很容易导致下面的访问的变量是一个不确定值。所以为了保护这种情况,C++系统就明确规定,要想在case语句中定义变量,必须在另起一个子块,这样就保证的下面标号的块是不可访问的。

11、尽量使用typedef,而不是使用define:定义类型别名,typedef 比 define更安全,define只是一个预处理命令,仅仅就是宏展开,不会进行类型检查。比如:这样定义一个 typedef int * pint; 这样一个指针类型,如果 pint a ,b ; 这样声明 a, b 都是指针类型,但如果换成 define pint int*,这样的话,pint a, b; b 就成了不是指针类型了,而是一个int 类型变量。

12、内联函数认识:在C++中一般性是将内联函数放在头文件中的,每个CPP文件包含它后都能使用这个内联函数,如果这个内联函数简单,那就会在每个CPP文件中进行代码展开。如果这个内联函数比较复杂的话,在VC6.0中,编译系统会在一个隐式的CPP文件中编译这个函数,其他文件直接调用。将一个内联函数写在一个CPP文件中不是一个好办法,因为inline是一个函数定义性关键字,编译系统当编译到这个函数时,发现这是一个内联函数,会重新去找哪些地方调用了这个函数,如果将这个函数声明放在了其他函数体中,那编译系统就不会找到这个地方,从而也不能进行代码展开。

13、头文件与实现文件的合理利用认识:一般性 类型定义(包括类的定义)、全局变量声明,函数原型、直接常数的常量定义、内联函数、宏定义以及后面要讲到的模板函数、模板类的实现定义都应该放在头文件中,而对于全局变量定义,函数体定义、类类型实现定义都应该放在CPP文件中。整个项目工程这样来管理文件是比较合理的。

二、类类型构造

C++语言与C语言的一个重大区别就是支持了类类型,类类型被定义为抽象数据类型,它有效地将数据以及作用于此数据的方法视为一个单元。我们可以抽象地考虑类该完成些什么,而无须去关心类是怎么实现的。与其他的面向对象的编程语言,C++的这种机制是更加的强大与复杂。它支持内置操作符可重载,以及支持多重继承等特点。C++中还引用了友元的机制,这种机制在一定程度上方便了类与类之间的协作,但又同时破坏了一定的封装性,所以使用时一定要把握住一个度。C++中的struct结构类型原则上是与class类类型一致的,无非就是默认的访问属性不一样而以。以下我就这些特性中有个关注点做一个总结:

1、构造函数、析构函数认识: C++中的构造函数、与析构函数与Delphi语言中是有所区别的,首先构造函数、析构函数是不会被派生类所继承的。每个派生类都会有自己的构造与析构函数,且会隐式调用基类中的构造与析构函数。且C++中的构造函数、与析构函数是系统会自动调用,不像delphi中需要create、free程序员手动来编写。(其实delphi中的对象是永远在堆中的,不像C++中这么灵活,可以在栈中、堆中、静态区中都可以)。C++中还有一个explicit特殊关键子,是指明显示转换为该类对象。比如:

class A
{
public:
  A(int a)
  {
    c = a;
  }
  void Set(const A& src)
  {
    c = src.c;
  }
private:
  int c;
}

这样一个类类型定义,当定义这样的一个A a对象时,a.set(2); 也是合法性的,首先编译系统会隐式转换为类A的临时对象,因为类A有个参数为int型的构造函数。并将这个临时对象转给Set函数。而当构造函数用explicit关键字明显指明时,则上面的函数调用就是非法的,必须a.set(A(2)); 这样调用才是合法的。所以我们在定义构造函数是否加入
explicit关键字,主要看隐式转换是否存在安全隐患,需要要让用户明显指明这里需要转换,而不是让编译系统不知不觉完成。

2、构造函数初始化列表认识:C++引入了构造函数初始化列表主要是因为构造函数不会继承的,每个派生类都有一个自己的构造函数,当基类中的构造函数有参数列表的话,则必须显示通过初始化列表来初始化基类,否则编译会通不过。注意:只要是派生类或者有内含对象时,初始化列表永远存在,无非就是显式、隐式的问题。初始化列表中显示的初始化顺序不是内含对象,或者派生类的初始顺序,真正的执行顺序是根据派生类继承基类的顺序,还有就是内含对象的声明顺序为依据。所以千万不要想在构造函数体中去初始化内含对象,或者基类的数据成员,这只会画蛇添足,影响性能罢了。

3、静态成员认识:C++中不但有静态类方法,而且还具有静态类数据成员,这是有Delphi有所区别的。对于类方法与类数据成员与全局函数与全局变量没有什么区别,只是作用域在类中,需要遵循类的访问级别而以。在类中可以定义一个静态常对象,如:static const int a = 100; 这个跟外部常量没有什么区别。只是它可能在类内部使用,也是直接将这个常量翻译成直接数的。

4、常成员定义认识:在C++中可以定义一个常成员函数,意思表明该函数可以作用于常对象,且这类函数不能改变类成员数据,也不能调用非const成员函数。一般性这类函数是返回类数据成员的。而且const成员函数也列入可重载函数机制,所以const关键字在类成员函数定义中一个唯一放在函数声明后面,且声明与定义都必须包含的。C++中也可以定义一个常量据成员,像这种常量数据成员必须在类的构造函数的初始化列表中进行初始化,其他地方都不能进行赋值,包括构造函数体中。像这种数据成员在类中只能读取,不能更改。

5、类中定义类型认识: 在C++中类中可以定义一个子类型,包括类类型。在类中定义子类型,一般使类更加抽象,即在使用这类类型时一般都是使用该类中的一套子类型,通过类作用域操作符::来访问类的子类型。而在类中定义一个子类(熟称嵌套类),一般性这些子类都是私有的,外部用户都不能访问的,因为这些类一般性都是外部类的具体实现类。外部类只是一个壳子,定义了外部接口而以。

6、具体类型与抽象类型认识:具体类型也就是数据都是具体的而且都是公开可见的,且没有相关的操作方法对此进行抽象封装,外部用户可以直接访问这些数据。比如学生信息结构体,这么一个类类型就是一个具体的结构体,外部可以直接来操纵这些成员。而如果定义一学生类,这个类就是一个抽象的数据类型,它对解决学生管理问题进行了一个抽象,通过定义增加、修改、删除这三个基本操作的外部接口对学生管理进行了封装,外部用户不必关心类内部的具体实现,而只要知道怎么操作外部接口就可以了。

7、向前类型声明认识:向前类型声明也只是仅仅说明了这个标识符是一个类类型,而不是其他类别。但在需要明确规定能知道类大小的地方不能使用类向前声明来定义对象,只能定义该类的指针或者引用,或者用在函数原型声明的参数类型中。比如:

class B;                             
class A
{
   B b;
   int a;
}                             

这种在定义类A时就是有问题,因为对象b要明确知道它的大小,而类型B只是一个向前声明,现在还没法明确知道它的类结构是怎么样的。所以这样的定义是错误的,如果将B b; 改为B *b 或者 B &b; 这样定义的话就是正确了。因为这时不需要明确知道类B的大小,只知道它是一个类类型就可以了。因为指针本身占据4个字节就可以了。

8、隐含的this指针认识:在C++中,这是一个非常重要的隐式参数,这与Delphi中的self参数作用一样。C++也正是通过了这个参数来实现每个对象的数据成员隔离,且互不影响,但每个对象方法共享的机制。这个参数是编译系统统一负责管理的,一般性程序员不要负责管理,只有在特殊需要使用的地方,开发人员才会显示地使用这个this指针来完成特要功能。注意:this指针指向常对象,或者对象。当成员函数为非const成员函数时,指向的是对象,即为 类型*const this,当成员函数为const成员函数时,指向的就是常对象,即为const 类型 * const this。从这里可以看出,不管指向的是对象还是常对象,this指针本身就是常指针,不能改变的。

9、可变数据成员认识:该规则主要是为了让const成员函数也能破例改变类中数据成员,也就是为const成员函数放宽了一小点点限制,让它能操作有mutable关键字声明的类数据成员变量。

10、类成员定义中的名字查找规则认识:类中作用域也存在的嵌套关系,也就是在类成员函数中定义的局部变量会覆盖类本身的数据成员,而且派生类中定义的成员函数如果与基类的定义一样,就算是参数类型、个数都不一样,也不能实现函数重载,这是与delphi中所区别的。如果要访问基类中的成员函数就必须通过基类::作用符来访问。在C++类定义体中,整个都属于类作用域。如果在类定义体中直接定义了一个函数,而它访问数据成员在这个成员函数后面定义的,这也是合法的,因为编译系统会到整个类定义中去找,然而一个类中定义了子类型,如果在类定义体中使用这个类型,则也必须满足在使用这个类型时,此类型必须已经在前声明了。

11、拷贝构造函数、赋值运算符、析构函数三者认识:这三个函数可以说是孪生兄弟,几乎是同时存在的。在一般性的类中,开发人员也许是不需要编写这几个函数的,编译系统会默认帮我们合成一个。但一般性类中有指针时,则就是需要自己各编写一个满足自己需求的函数了,否则如果使用系统默认生成的函数就存在安全隐患了。在这里非常要强调的一点是,当自己定义了拷贝构造函数,如果该类中有内含对象或者是一个派生类,则拷贝构造函数必须要有一个初始化列表,显示来调用内部对象的或者基类的拷贝构造函数,否则内部对象或者基类的拷贝构造函数不会调用,这样就导致了该类中部分没有拷贝的结果。同样赋值运行符也是同样道理,如果该类自己重载了赋值运算符,则必须显示调用内含对象的赋值运算符与基类的赋值运算符,否则该类就只有部分被赋值了。然而对于析构函数是一个特例,当该类定义自己的析构函数,不需要显示调用内部对象与基类的析构函数,编译系统会默认帮我们调用。本人感觉,这在C++类中最为复杂的三个函数,一般性类较为复杂时,才需要自定义编写,且需要一定的功底,否则很容易引起安全隐患。

12、禁止类复制的技巧:有时在一些特殊情况下,需要防止类的复制的功能,以防止存在的安全隐患,这样就可以在类的定义体中声明一个拷贝构造函数(且没有去实现它),一般性这个函数要声明在私有部分的,否则在编译的时候是不会有错误的,直到程序链接时没有找到真正的函数体而发生链接错误。

13、智能指针实现原理:在C++中指针是非常重要,且非常灵活的。C++中正式用了指针与引用来实现面向对象特性的,而不像delphi本身就是使用对象来实现面向对象特性。但使用指针往往是很不安全的,一步小心就掉进陷阱里去了,而且还很不容易查找。所以一般使用引用计数技术来实现智能指针,用户不用关心何时该释放对象,也不用担心现在使用指针使用对象是否合法。以下是一个实现智能指针的简单类:

template <class T>
class SmartPointer
{
public:
  SmartPointer(T* lpPointer): m_lpPointer(lpPointer)
  {
    m_nUse = 1;  //指向一对象时,初始值引用计数为1
  }
  ~SmartPointer()  //析构函数不马上释放对象,先检查引用计数,如果引用计数为0,也就是对象不再使用了就释放
  {
    if (--m_nUse == 0)
    {
      delete m_lpPointer;
    }
  }
  SmartPointer(const SmartPointer &Src)
  {
    m_lpPointer = Src.m_lpPointer;
    m_nUse = ++Src.m_nUse;
  }
  SmartPointer& operator=(const SmartPointer &Src)
  {
    if (--m_nUse == 0) //赋值之前先检查自身的引用计数,如果为0了就释放对象
    {
      delete m_lpPointer;
    }
    m_lpPointer = Src.m_lpPointer; 
    m_nUse = ++Src.m_nUse;
    return *this;
  }
  T* operator->() const
  {
    return m_lpPointer;  //这里没有检查指针的合法性,我只是简单化,一般性要检查,不合法就发生异常
  }
  T& operator*() const
  {
    return *m_lpPointer; //这里没有检查指针的合法性,我只是简单化,一般性要检查,不合法就发生异常
  }
private:
  T * m_lpPointer;
  int m_nUse;
};

 

14、操作符重载认识:C++中一个重大特点就是可以操作符被重载,这是其他语言所没有的。重载操作符使语言更加富有灵活性,使代码更精简,更具有可读性。操作符重载一般性其中一个类型为类类型或者结构类型,不能重载两个参数都为int型的+操作符,因为C++本身已经内置了对这两个操作数类型的+操作符,开发人员不能将其改变。一般性将运算操作符重载为非成员函数,因为运算操作符一般性左右操作数都可以相互交换的。而对于其他的操作符重载,一般性为成员函数,特别是[]、()、*、->、(类型)等这些操作符。还有就是像::、sizeof、? : 、.这几个操作符在C++中规定是不能重载的。

15、重载 &&、 ||、& 操作符不是好的做法: 对于这几个操作符一般性不要重载,因为像 && || 这个两个操作符本身内置的是有计算顺序的,即必定先计算左操作数,然后根据左操作数的值来确定是否计算右操作数。而如果重载了这两个操作符,这种计算顺序就会变得不确定性,所以建议不重载。而对于&操作符它本身的含义就是取对象地址的,如果重载此操作符,就会失去它本身的含义。

16、输入输出操作符的重载规则:对于这两个操作符必须重载为非成员函数,因为像这两个操作符对于输入输出流肯定是在左边的,所以必须将这两个操作符重载为非成员函数,且必须将流对象置为左参数。如下:

ostream& operator <<(ostream &out, const myclass& myobject)
{
}

17、调用操作符重载: 调用操作符可被重载,大大增强了语言的灵活性,这也是标准库中关于算法库的有力支持。该操作符重载是唯一一个不需要规定其操作数是多少个的,且可以具有默认实参值的操作符。当类重载了调用操作符可以有好几个版本,具体区分是根据参数类型,与参数个数来区别。如果类重载了调用操作符,这个类对象就叫做函数对象了,因为可以通过对象()来调用了,就像调用函数一样。

18、转换操作符重载:本身类可以通过构造函数将其他类型转换为类本身类型,也可以通过转换操作符将类本身类型转换为其他类型。通过重载转换操作符是大大简化了类本身的设计复杂度。比如一个类想与数值型数据进行算术运算,如果不使用重载类型转换操作符,则该类想支持这些操作,无疑是要面面俱到的重载各个算术运算操作符,而且很不灵活,增加了类本身的负担。如果重载将转换为int型转换操作符,则该类可以与其他数值型进行C++系统内置支持的各种各样的算术运算,即简化了类,又增加了类本身的灵活性。

三、面向对象编程与模板构建

1、对象之间的赋值兼容规则:  在C++中,对象赋值兼容是面向对象多态性的有力支持, 只要派生类是公用继承的,派生类对象实例就可以赋值给基类对象实例,其中就是赋值了在派生类对象中的基类部分。 而对于指针与引用,则对于派生类的指针或引用编译系统都会自动转换为基类的指针或引用。而一个基类对象是不能赋值给一个派生类对象的,因为基类对象不包含派生类对象的扩充部分,对象实例直接不存在转换。但是可以将一个基类的指针或引用转换为派生类的指针或引用,这个转换不是编译系统隐式转换的,而是需要开发人员明显指定的显示转换,在这里建议不要强制转换,而是通过dynamic_cast操作符来进行动态转换。

2、访问标号与派生类别的规则:在C++中,对于类的访问权限与其他高级编程语言没什么区别,也就是包含了private私有部分,protected 保护部分,public 公用部分,而对于派生类别是C++语言中特有的,一般性也是有private、protected、public三种类型的派生,对于priavte继承,一般性派生类是对基类的一种实现,不过这种情况的继承不是多见的,而对于protcted继承,一般性是想将基类的方法对于外部用户隐藏起来,然而开发人员可以通过using声明把部分方法再次公布出来,比如在派生类中的公用部分用一个using  基类::方法名,则基类中的这个公共方法在派生类中也可见了。关于公用继承就是原封不动的继承到派生类中,原有基类部分在派生类中不会改变属性。

3、覆盖虚函数机制: 当在类中定义了虚函数时,如果在该类中的其他函数中调用此虚函数时,是进行动态绑定的,只有到了运行时才知道是运行哪个函数。而如果开发人员需要明确指定此虚函数的本类版本 时,就需要通过类作用符进行限定,这样编译系统就不会进行动态绑定了,而是静态绑定了,也就是说在程序链接的时候就是静态链接了此版本的函数地址。

4、虚函数使用默认实参的注意点:一般性给虚函数的参数设置默认值,基类与派生类的版本必须是一致的,否则就会给自己埋下安全隐患,因为当你用基类的版本去调用虚函数的时候,虽然可能在运行时调用的是派生类的版本,但默认值传进去的还是基类版本中的值,这样很可能会导致派生类的这个函数版本发生逻辑错误,因为它原本设计是不是传进这个值的,而是传进自己重新设个的那个默认值。

5、派生类拷贝构造函数与赋值运算符被显示定义:  当显示定义了派生类的拷贝构造函数、赋值运算操作符后,在派生类中务必要显示调用基类的拷贝构造函数,与基类的赋值运算符 ,当在派生类的拷贝构造函数中没有显示调用基类的拷贝构造函数,编译系统就会默认调用基类的默认构造函数(没有默认构造函数时,就会编译错误),这样拷贝一个对象就是一个两面对象,一部分是来自原对象的拷贝数据,一部分是来自基类的构造函数。而当重定义了赋值运算符后,要显示调用基类的操作符,基类:: operator=(参数值),这样基类部分才会被赋值,否则基类部分就是没有操作,也是一个两面对象,这样的对象很危险,一不小心就会发生错误。 

6、关于构造函数与析构函数调用虚方法:C++中关于在构造函数与析构函数里面调用虚函数时是采用静态绑定的(这是与delphi语言有所区别的),从面向对象角度来解释的话,因为虚函数只有针对对象才具备多态性。而在构造函数中,对象本身就还没有构造完毕,所以在构造函数中调用虚函数是静态绑定也是理所当然了。而在析构函数中,对象也是变得不完整了,比如在调调用基类的析构函数时,派生类部分已经进行析构了,如果这时在通过虚函数调用派生类版本就会很可能发生错误了。所以这样看来,在这块机制上C++比delphi安全的多。

7、当派生类重定义基类函数时: C++中,如果当派生类重新定义了函数的另一个版本时,不会进行重载,而是进行了覆盖,也就是把基类的函数都屏蔽掉了,当在派生类中调用此函数时,只能用本身版本,除非使用了基类作用符加以限定。如果想通过派生类也能调用到基类的版本,也就是说实现了重载,那必须在派生类中使用using 基类:;方法名,来将基类中的方法引入到派生类作用域中,这样就实现了与基类版本进行重载,而不是覆盖, 也就是在派生类中可以直接调用基类中的版本了。

8、句柄类通用技术:   一般性我们在使用C++标准库中的vector类时,如果指定的仅仅是某一类类型,那这个容器里面保存的就是这个类型的统一对象,这样显得保存这些对象也毫无意义,但如果指定的是某一类类型的指针,就显得有些不安全,因为这样需要开发人员自己来管理内存,否则很容易造成内存泄漏。在这里有个通用的解决办法就是设计一个句柄类,来统一管理这个类类型的继承层次,并负责类的创建于释放。例子如下:

template <class T>
class CHandle
{
public:
  CHandle(): m_lpObject(NULL), m_lpUser(new int(1))
  {	
  }
  virtual ~CHandle()
  {
    if (--*m_lpUser == 0)
    {
      delete m_lpObject;  
      delete m_lpUser;
    }
  }
  CHandle(const CHandle &Src)
  {
    m_lpObject = Src.m_lpObject;
    m_lpUser = Src.m_lpUser;
    (*m_lpUser)++;
  }
  CHandle& operator=(CHandle &Src)
  {
    if (--*m_lpUser == 0)
    {
      delete m_lpObject;  
      delete m_lpUser;
    }
    m_lpObject = Src.m_lpObject;
    m_lpUser = Src.m_lpUser;
    (*m_lpUser)++;
    return *this;
  }
  //这里用到了克隆技术,也就是说T类类型有个Clone虚方法,来克隆自己,这是技术关键,
  //只要所有派生于T类的子类覆盖此方法,都能正确返回自己对象的克隆版
  CHandle(const T &Src): m_lpObject(Src.Clone()), m_lpUser(new int(1))    
  {
   
  }
  T& operator*()
  {
    return *m_lpObject;  //这里没有检查指针的合法性,原则上需要检查的
  }
  T* operator->()
  {
    return m_lpObject; //这里没有检查指针的合法性,原则上需要检查的
  }
private:
  T *m_lpObject;
  int *m_lpUser;
};

9、泛型编程: 在C++中,模板机制是泛型编程的有力支持,模板实现了类型参数化,即为类型多态性。但这个多态性与虚函数的多态性有着本质区别的,类型多态性是编译时期的静态多态,编译系统在编译阶段根据传入的类型就能构造出一个新的类型,而虚函数多态是动态多态,只有到了运行时才能知道真正运行的是哪个版本的函数,在编译期间编译系统是无法确定的。

10、关键字typename与class的区别:typename 与class关键字的作用是一样的,没有什么区别,两者可以互相调用,class关键字只是历史原因存在,在没有标准C++定义之前,用的是class关键字,后来定义了标准C++后,使用typename来代替class关键字,不过我还是习惯在定义类模板时使用class关键字,在定义函数模板时使用typename关键字。

11、非类型形参模板:在模板参数表中还是可以支持非类型参数,也就是传进去的不是一个类型,而是一个在编译期间可以计算出的一个常量值,通过这个常量值可以生成不同的模板实例。比如现在有一个模板函数,它的参数是一数组的引用类型,所以可以引用不同的基类型,但数组的维数大小必须是指定的,这样模板就显的有些不通用,这里就可以使用非类型形参来解决此问题,例子如下:

template <typename T, int N> 
void Setva(T (&aar)[N])
{
  //此模板在vc6.0下是不支持的,因为vc6.0是98年出的,对标准C++支持的不是很好。
}

12、类模板与模板函数实例化区别: 类模板的实例化是通过在定义类对象时,通过显示传入模板类型实参来确定实例哪种类。而对于函数模板的实例是可以通过调用函数的实参类型来推断出哪个函数实例的,当然也可以显示推断,也就是说可以显示指定函数参数类型。这样调用函数的实参就不起作用了,如下:

template <typename T>
T getrealv(T a)
{
   return a;
}
//这里2.3实参不起作用了,因为有了显示实参指定,所以2.3会转换为int类型,精度就丢失了。
double k = getrealv<int>(2.3);

13、模板函数推断与函数指针:在C++中可以将一个模板函数赋值一个函数指针,它是通过定义函数指针的类型来实例化这个模板函数的,不过这种方法很容易产生二义性,比如以下这种情况,编译系统就不知道怎么生成一个函数实例了。

template <typename T>
bool Compare(const T &a, const T &b)
{
   return false;
}

void func(bool (*Compare)(const int&, const int&))
{
  
}

void func(bool (*Compare)(const double&, const double&))
{
	
}
//在这里可以生成两个版本,所以就产生了二义性。
func(Compare);

14、模板编译模型: 在标准C++中存在两种编译模式 ,一种是包含编译模式,一种是分别编译模式,  而现在所有编译系统都是支持第一种编译模式的,仅有个别的编译系统支持第二种编译模式。 包含编译模式,也就是模板类必须定义在头文件中,需要使用这个模板的cpp文件都需要包含这个头文件。   但在定义这个模板类的时候,不一定需要在类体内定义,也可以将函数成员在类体外定义的,无非这个模板类必须在同一个文件中而以。

15、类模板成员何时实例化:当我们定义一个模板类的时候,在通过这个模板类定义对象时,不是所有的部分都实例化的,而是当使用到哪部分才实例化某部分。所以当我们现在编译通过的时候,千万不要认为我现在编写的这个模板类是正确的,可能到时哪天你在修改代码的时候发现怎么编译通不过了不要惊讶。比如我们在一个模板类中定义了两个模板函数,但我们只调用了其中一个,那另外一个模板函数是不会被实例化的。例子如下:

template <class T>
class CTemplate1
{
  //代码实现
};

template <class T>
class CTemplate2
{
public:
  void SetValue(const T a)
  {
    //调用此函数时才会实例化
  }
  T GetValue()
  {
    //调用此函数时才会实例化
  } 
private:
  //注意这里是指针,如果是对象的话,实例外部模板类时,这个类也会跟着实例化,
  //但如果是指针的话,只有当使用此对象指针时才会实例化该模板类
  CTemplate1<T> *m_a;  
};

16、类模板中友元声明: 关于模板类的友元声明比较复杂些,大致可以分为3种情况,(1)当在模板类中将普通类或者普通函数声明为友元时,则这个普通类或者普通函数都可以访问模板类的所有实例。 (2)当在模板类A中将另一个模板类B声明为友元类时,则这个模板类B的所有实例可以访问模板类A的所有实例。(3)当在模板类中将另一模板类或者模板函数的特例声明为友元,则这个模板的特殊实例才可以访问这个模板。如下:

template <class T> class CTemplate1; 

template <class T>
class CTemplate2
{
public:
  friend class CTemplate1<int>; //也就是只有int型的这个模板实例才是友元类
};

template <class T>
class CTemplate1
{
  //代码实现
};

17、成员模板:当在模板类中再定义自己的函数模板就是成员模板了,也就是这个函数模板跟外部的模板类没有类型直接关系,而是通过自己的函数调用的实参类型来推断出自己的函数实例。这种技术在C++标准库的容器类中特别有用,比如可以将vector<int>型的容器可以去初始化vector<double>容器。简单例子如下:

template <class T>
class CTemplate1
{
public:
  template <class A>
  CTemplate1(const A &a)
  {
     m_a = a;
  }
private:
  T m_a;
};

CTemplate1<double> Template1(12.3);  //这里会实例化一个double型的构造函数
CTemplate1<double> Template2(12);   //虽然成员是double型的,但这里会实例化一个int型的构造函数

18、类模板中的静态成员:像在普通类中的静态成员一样,在类模板中也照样可以定义静态成员,当在模板类中定义静态成员时,每个模板类的实例都会包含一个静态成员,而且互不干扰。也就是这个模板类的所有对象共享这个实例类的静态成员,那个模板类的所有对象共享那个实例类的静态成员。

19、模板特化:  在C++中,一般性定义一个模板时不能完全支持所有类型,可能需要对某些类型进行特殊处理,这就是所谓的模板特化。其中包括函数模板特化,类模板特化,类模板中部分特化等几种情况。对于模板特化对于用户来说是透明的,用户在使用模板的时候,不需知道它有哪些特例,模板特化只是为了保证模板实例化后程序能够正常运行而以。

template <typename T>
bool Compare(const T &a,  const T &b)
{
  return a == b;
}

template <>
bool Compare(const char* const &a, const char* const &b)
{
  return strcmp(a, b) == 0; //对于字符指针类型进行了特化,因为仅仅比较指针中的值是毫无意义的,需要比较字符串值
}

bool bRet =  Compare<const char*>("kkk", "aaa");  //调用了模板特化那个版本
template <class T>
class CTemplate1
{
public:
  void SetValue(T a)
  {
    m_a = a;
  }
  T GetValue()
  {
    return m_a;
  }
private:
  T m_a;
};

template <>
void CTemplate1<double>::SetValue(double a)
{
  m_a = a + 100; //模板类中部分成员特化,也就是当实例化一个double型的类时,调用这个版本
}

CTemplate1<double> Template1;
Template1.SetValue(1.8); //调用特化版本

20、重载与函数模板:  一般性不建议函数模板与普通函数进行重载,这样会显的程序很不清晰,用户会觉得扎乱无章,最好是通过函数特化来代替普通函数重载。现在仅将普通函数与模板函数同时重载时定位的规则作一介绍,当普通函数能完全符合时,则高于模板函数,模板函数就不会再实例化一个,而是匹配了普通函数进行调用。如下:

template <typename T>
T add(T a, T b)
{
  return a + b;
}

template <typename T>
T add(T a, T b, T c)
{
  return a * b + c;
}

int add(int a, int b)
{
  return a + b + 100;
}

int k = add(12,35); //调用普通函数add,而不会根据模板函数去实例化一个。


四、C++标准库

 C++标准库虽然不是C++语言本身范畴,但是它是C++编程的重要组成部分。为什么要引用C++库呢,一主要是为了提高开发效率跟稳定性,二是主要为了提高C++程序的可移植性,只要所有的编译系统都遵循C++标准库规范,那C++程序即可以在不同的平台上移植。在C++标准库还没有定义出来前,C++用的是C运行时库,现在标准C++库主要由STL标准模块库,Iostreams输入输出流库、其他类库、C运行时库的包装库这四大部分组成。其实C++程序还是需要C运行库支持的,C++没有重新去实现一遍C运行时库,而是对它进行了一个包装,统一遵循C++标准规范。比如在C++程序要使用malloc这个C运行时库函数来分配内存,则需要包含c运行时库的C++版的cstdlib头文件(在前面加入一个c,只是为了说明这头文件中的功能来自c运行时库),而不要包含stdlib.h头文件,其实cstdlib头文件中包含的就是stdlib.h,而如果直接包含stdlib.h,只会给c++程序带来不可移植性。还有重要一点就是C++标准库引用了新的输入输出流库,而以前c运行时库也包含这部分功能,但现在C++标准库覆盖了c运行库时,里面又需要C运行时库的支持,对程序员来说c运行时可以是透明,只需要考虑C++标准库整个框架的功能即可。但如果又想使用C运行库中的这部分功能怎么办呢,所以标准组委会干脆将这部分功能从c运行库中抽离出来,另起了另外一个独立库Old iostream Library,如果程序员想使用老式的流库,就包含这个库中的头文件。但不能与C++标准库中新的流操作混合使用,它们俩是不兼容的,所以在程序中最好要么使用新式的流库,要么使用老式的流库。以下我就VC对这些库支持的情况作一介绍,在VC中MSVCRT.DLL实现了C运行时库,MSVCP60.dll实现了C++标准库,MSVCIRT.DLL实现了老式的流库,在用VC写的程序必须需要MSVCRT.DLL的支持(其实main不是真正的入口函数,真正的入口函数是在运行时库中,在进入main函数时,c运行时库其实还做了其他初始化事情,在链接vc程序时,链接的是运行时库入口函数的代理,这个代理函数会跳转到入口函数中执行),但C++标准库与Old iostream Library库是可选的,主要看你有没有包含相关的头文件。以下是一个vc程序所使用库的关系图:

                

                                                                                                                                                                                                                      
 1、STL标准模板库:标准模板库主要是容器类与泛型算法库两大部分组成。容器类有分为顺序容器类与关联容器类,其中顺序容器类又引申出了顺序容器适配器。目前顺序容器类主要为vector、list、deque三个模板类,关联容器类主要为map、set两个模板类,适配器为stack、queue、priority_queue三个模板类。在这些容器类中元素类型是有限制的,一般性必须支持赋值与拷贝构造功能,对于关联容器类其元素类型还必须支持<操作符运算。关于容器类的元素操作引入了迭代器感念,通过迭代器可以遍历容器中的各个元素,并对其进行操作。迭起器看起来很像是一个指针,事实上它也是对指针一个跟高级版本,所以在操作迭代器时不正确使用可能会引起意向不到的错误。在容器类本身中只是加了简单的增、删、改等操作,而像其他一些更高级的操作,比如排序、查找、最大值、最小值查找都是归属在算法库中,因为这样做更能使代码通用型,这些泛型算法可以处理符合标准的各个类型,包括自己自定义的类型。

2、Iostreams输入输出流库:该部分主要是对流机制的封装,是新式的流操作,与Old iostream Library中的老式流操作是并行的, 其中包含了文件流(fstream)、标准输入输出流(iostream, cin,cout就是这个流的真实对象,在c++用cin、cout来进行标准输出输出,而不是用C运行库中scanf、printf库函数来完成。),字符串流(sstream)。

3、其他类库:该部分是C++标准库中另一部分公用类库,其中涉及到的范畴主要为位操作<bitset>、复数操作<complex>、异常操作<exception>、内存管理<memory>、字符串操作<string>。在c++中对于字符串的处理最好使用string类,这样既安全有稳定性高,使用运行时库那批字符串函数虽然是低级操作,到安全性不高,开发人员一不小心就会使程序发生莫名其妙的错误。

4、C运行时库函数的包装库:在C++程序中一般性调用C运行库函数时,不要直接包含C运行时库的头文件,这样只会给C++程序带来不可移植性,而要包含C++版本的c运行时库头文件。这样做的好处就是统一了C++程序的规范,一同认为这些都是C++标准库的函数功能,不用纠结这些函数是运行时库的,这些函数是标准库中的。一些列出了对应c运行时库头文件的c++版本:<cassert>、<cctype>、<cerrno>、<cfloat>、<cmath> 、<cstdarg>、<cstddef> 、<cstdio> 、<cstdlib> 、<cstring>、<ctime>、<cwchar> 等。

五、其他高级工具

在C++编写大型程序时,系统就会变得非常庞大与复杂,这时难免会用到一些高级工具,比如通过异常机制可以使程序变得更加健壮、稳定,以致于不会在程序发生错误时,导致程序直接退出而不能继续往下执行。通过命名空间机制,可以使程序通过不同的库组装而成,而不会发生名字污染问题,命名空间无疑为开发系统增添了灵活性。而对于运行时类型信息,可以使类型转换变得更加安全,而不会导致莫名其妙的错误。C++中预分配策略可以使程序在需要一定性能的情况下发挥一定作用,使得在频繁创建对象与释放对象得到一个平衡。

1、异常机制:在C++中通过try、catch、throw三个关键字来实现异常机制,其实这是编程语言对windows系统的SEH异常处理机制的包装(delphi中也是这种包装机制),使得在C++中处理异常变动更加简单,开发人员可以合理地将检查异常的地方与处理异常的地方有效地分开。 在C++中异常不是等于真的发生了错误,而是仅仅遇到throw表达式,当遇到throw表达式时,异常机制就马上启动,它停止了往下执行语句,而是一层层往上查找符合异常类型的处理块,如果没有找到用户定义的异常处理块,就会找到运行时库中,它会弹出一个异常框,并退出了程序。当匹配到最近一个异常处理块中,异常栈就会展开,它会一个个回收每层函数中的局部资源,当然其中也会调用每个局部对象的析构函数(注意如果在析构函数中又发生异常的话,这样就会导致直接跳转到运行时库,退出程序,所以C++标准库中所有类明确规定析构函数不会发生异常,目的就是增强程序的稳定性)。在异常抛出与异常处理之间的通信是通过异常对象来实现的,异常对象是编译系统来维护的,当throw 表达式时,就会自动创建一个异常对象,并将表达式的值拷贝到异常对象中,所以对于类类型来说,必须要有拷贝构造函数,否则异常机制就无法完成。当catch异常说明符中有形参时,只要匹配到了这个异常处理块,编译系统就会把这个异常对象传给这个形参,就像函数调用一样。所以如果这个形参是一个引用类型的话,它就直接引用了这个异常对象,如果在异常处理块中通过throw又重新抛出了异常,这个异常对象就会重新再往上层一层层匹配。还有catch(...)能捕获所有异常,这个一般性与重新抛出异常一起使用,因为像这种情况下一般性不是处理异常,而是对自己函数内部进行善后,完了再把异常抛出去给能真正处理异常的处理块进行处理。在C++中定义函数时可以明确指明这个函数能抛出哪些异常,比如 throw()就是代表不抛出任何异常,当在函数中抛出异常与异常说明列表中不匹配的,就会直接跳转到unexpected库函数,退出程序。C++标准库定义了一个exception异常基类,在C++标准库中抛出的所有异常都是继承与该类的。在C++标准库中有个auto_ptr 异常安全资源类,这个类的作用就是能在函数中new分配的内存能安全地释放到(只要把得到的指针设给它就行),原理就是在异常栈展开时会自动调用每层函数的局部对象的析构函数。

2、命名空间:命名空间的引入就是为了解决当程序变得非常强大时,使用各个模块库组装而成引起的名字冲突的污染问题,记住一个命名空间就是一个作用域(从作用域大小来看,全局命名空间作用域最大,是项目作用域,其次是文件作用域,其次是在每个文件中自己定义的命名空间作用域)。其实在C++的每个项目中都有个全局的命名空间,当我们在文件作用域中定义的全局对象,或者定义的类都是在一个全局命名空间中,而我们自己定义的有名命名空间还是无名命名空间都是在内嵌在这个全局命名空间中,也是说全局命名空间包围了整个项目。当为::全局访问符时访问的就是全局命名空间中的成员。命名空间可以不连续的,也就是在一个文件定义了一个名称为aaa的命名空间,在另一个文件中再定义这样aaa的命名空间,不是另起一个命名空间,而是重新打开了刚才在那个文件中定义的aaa命名空间,再往里面加东西而以。未命名命名空间是有特殊含义的,它是特定于某个文件的,访问未命名空间不需要空间名限定符,直接访问就可以了。比如里面的定义的变量相当于static定义变量,就是局限当地文件的,其他地方是看不到的。在标准C++中建议使用未命名空间来定义静态全局变量,而不是用static关键字,这样有利于C++程序的可移植性。在使用命名空间中的成员时建议使用using声明,而不是用using指示,使用using指示很容易又造成名字污染问题,而且使代码很不稳定性,说不定哪天加入另外一个库时,就会导致代码编译通不过了。当通过using声明将命名空间中本身函数重载集引入到当前文件作用域中,如果在当前作用域中有定义了同名函数,则会跟命名空间中的函数一起重载。

3、内存预分配策略:在C++中可以通过标准库中allocator类来完成预分配功能,比如通过allocator.allocate来完成分配指定类类型的多少个缓存大小,而通过construct()方法来拷贝构造一个缓存对象,所以对于使用allocator类来完成预分配的,必须类类型具备拷贝构造函数。通过destroy()函数来析构缓存对象,也就是该函数会调用缓存对象的析构函数,最后通过deallocate()函数类释放刚才allocate分配的缓存。这个类虽然能完成预分配工作,但具备一定的局限性,比如只能通过拷贝构造函数来构造缓存对象。可以通过标准库的operator new delete 标准库函数来完成预分配功能,首先来讲解下关于C++中定义的new 、delete操作符运行机制,当我们new 类型名时,编译系统首先会调用标准库中的operator new函数(注意:标准库定义的operator new函数绝不是重载new操作符,new操作符是C++内置的,绝不能重载,它的运行机制就是调用这个标准库函数),如果是类类型,还会调用类的构造函数来完成对象的构造,最后返回这个指针,delete操作符也是同样道理。所以我们可以直接调用标准库中的new函数来分配内存,再通过定位new表达式来构造对象(定位new表达式是new操作符的那一个版本,语法为 new(地址)类型名(), 它就是在这个指定地址完成构造对象),当需要这个对象时,可以直接调用析构函数类析构对象,而不释放内存。这种模式也些复杂,我们还可以通过另外一种途径,就是在自己的类中定义new、delete操作成员,来特化自己的分配策略。当类定义自己的new、delete成员时,new表达式就会调用类本身的new成员,而不会调用标准库中的new函数。注意类中new、delete成员默认都是静态函数,因为它们与具体对象没有关系。具体下面我编写了一个通用类来实现内存预分配策略,只要自己定义的类继承它,就有该功能。

template<class T>
class CCacheObj
{
public:
  CCacheObj(): m_next(NULL)
  {

  }
  virtual ~CCacheObj()
  {

  }
  void* operator new(size_t size)
  {
    if (size != sizeof(T))
    {
      throw runtime_error("无效的类型大小,分配对象失败");
    }
    if (freelist == NULL) //说明没有足够的空间分配对象了
    {
      T* lpTemp = objallocator.allocate(objsize,NULL);
      for (int I = 0; I < objsize; I++)
      {
        AddToFreeList(lpTemp + I);  
      }
    }
    T* P = freelist;  //得到自由列表中一个可用缓存对象
    freelist = freelist->CCacheObj<T>::m_next;
    return P;
  }
  void operator delete(void *pstr)
  {
    if (pstr != NULL)
    {
      AddToFreeList(static_cast<T*>(pstr));    
    }     
  }
protected:
  T* m_next;
  static void AddToFreeList(T *lpObj)
  {
    lpObj->CCacheObj<T>::m_next = freelist;
    freelist = lpObj;
  }
private:
  static std::allocator<T> objallocator;
  static T* freelist;
  static const int objsize;
};

template<class T>
std::allocator<T> CCacheObj<T>::objallocator;

template<class T>
T* CCacheObj<T>::freelist = NULL;
template<class T>
const int CCacheObj<T>::objsize = 25;

class CSrceen: public CCacheObj<CSrceen>
{


};


4、运行时类型信息: 在C++中可通过typeid 关键字来获得类型的运行时类型信息。每当在遇到typeid 类型名的时候,编译系统在编译的时候就会把这个静态类型的运行时类型信息放入到程序的静态区,typeid就是得到一个type_info类类型的常引用体。dynamic_cast动态转换就是基于运行时类型信息的,dynamic_cast动态转换时会根据指针或者引用指向的对象的运行时类型信息跟要转换的静态类型的运行时类型信息进行比较,如果相等就是转换正常,如果不相等,当我引用时,就发生转换异常,如果是指针就是转换获得空指针。注意当在dynamic_cast进行动态转换时,对象中必须要含有虚函数,否则就不能进行动态转换,编译无法通过。也就是说当类中有虚函数时,编译系统会将这些类的类类型信息放到相应的内存中(因为有了虚函数,就有了一张虚拟表,而且对象的隐式头四个字节是指向这张表的,估计是这张表的的偏移量位置处放着运行时类型信息)(在VC中,必须将GR+打开,否则还是不会生成,以致在使用dynamic_cast操作符时会发生内存访问错误,因为该操作符断定该类类型对应的相应内存中存在运行时类型信息。),并在使用typeid 指针、引用表达式时,不会在编译时将操作数的静态类型放入到type_info结构中。而是运行时动态去取运行时类型并放入到type_info结构中。但像内置类型,如int等,这种类型在使用typeid操作符时,始终是在编译时将静态类型放入到type_info结构体中。

 


 



 

 

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值