C/C++回顾总结之四

C++问题总结

参数入栈的顺序,为什么?

C++有两种常见的函数修饰__cdecl默认是这个)和__stdcall(这两个宏定义是VC++定义的),前者由调用者清理堆栈,后者由被调用者清理堆栈。两种调用方式参数都是由右向左入栈。像printf这种不定参数的函数,必须使用__cdecl的方式调用,因为被调用者不知道参数的个数,无法清理恢复栈内的信息。同时,由于不定参数的函数的定义中,只保留了对。。。前第一个参数的引用,所以从右至左的顺序可以知道栈顶的元素即是该参数。

内联函数

对于任何内联函数,编译器在符号表里放入函数的声明(包括名字、参数类型、返回值类型)。如果编译器没有发现内联函数存在错误,那么该函数的代码也被放入符号表里。在调用一个内联函数时,编译器首先检查调用是否正确(进行类型安全检查,或者进行自动类型转换,当然对所有的函数都一样)。如果正确,内联函数的代码就会直接替换函数调用,于是省去了函数调用的开销。这个过程与预处理有显著的不同,因为预处理器不能进行类型安全检查,或者进行自动类型转换。假如内联函数是成员函数,对象的地址(this)会被放在合适的地方,这也是预处理器办不到的。

 

关键字inline必须与函数定义体放在一起才能使函数成为内联,仅将inline放在函数声明前面不起任何作用

 

内联函数不能递归调用,并且也无法与virtual关键字放在一起。

引用与指针的区别

指针是一种特殊的数据类型,其需要由编译器分配内存空间,初始值可以指向一块内存,但也可以为空。

引用必须被初始化,指向另一个变量,是另一个变量的别名,在内存中并不占用存储空间。并且引用的值(即它作为谁的别名)一量被初始化,就无法在运行的过程中被改变。如果你需要一个初始值为空,或者指向的对象会发生动态改变的数据时,你应该使用指针而不是使用引用。相反如果你使用的变量初始值不允许为空并且运行的过程当中不会发生改变,你就应该使用引用而不是使用指针。

在进行操作符重载时应该使用引用。

 

不要返回在局部定义的对象的指针或者是引用。因为该对象在超出其定义域后会被析构,其所占用的内存会被回收。直接返回相应的局部变量即可,返回时会自动调用拷贝构造函数。

 

删除后的指针一定要设置为NULL,防止出现野指针(迷途指针)。

常量

在类中定义的常量必须在初始化列表中被初始化。

 

const也可以在成员函数尾部表明其是一个常成员函数,常成员函数无法修改类中的成员变量,除非这个变量是被mutable修饰的。

 

一个类的const实例,无法调用该类的非常成员函数。

 

1.         欲阻止一个变量被改变,可以使用const关键字。在定义该const变量时,通常需要对它进行初始化,因为以后就没有机会再去改变它了;

2.         对指针来说,可以指定指针本身为const,也可以指定指针所指的数据为const,或二者同时指定为const

3.         在一个函数声明中,const可以修饰形参,表明它是一个输入参数,在函数内部不能改变其值;

4.         对于类的成员函数,若指定其为const类型,则表明其是一个常函数,不能修改类的成员变量;

5.         对于类的成员函数,有时候必须指定其返回值为const类型,以使得其返回值不为“左值”。

类型转换

C风格的转换。允许你在任何类型之间进行转换。不过如果要进行更精确的类型转换,这会是一个优点。在这些类型转换中存在着巨大的不同,例如把一个指向const对象的指针(pointer-to-const-object)转换成指向非const对象的指针(pointer-to-non-const-object (即一个仅仅去除cosnt的类型转换),把一个指向基类的指针转换成指向子类的指针(即完全改变对象类型)。传统的C风格的类型转换不对上述两种转换进行区分。(这一点也不令人惊讶,因为C风格的类型转换是为C语言设计的,而不是为C++语言设计的)。

 

二来C风格的类型转换在程序语句中难以识别。在语法上类型转换由圆括号和标识符组成,而这些可以用在C++中的任何地方。这使得回答象这样一个最基本的有关类型转换的问题变得很困难,在这个程序中是否使用了类型转换?。这是因为人工阅读很可能忽略了类型转换的语句,而利用象grep的工具程序也不能从语句构成上区分出它们来。

 

C++通过引进四个新的类型转换操作符克服了C风格类型转换的缺点,这四个操作符是,static_cast, const_cast, dynamic_cast, reinterpret_cast。在大多数情况下,对于这些操作符你只需要知道原来你习惯于这样写,(type) expression而现在你总应该这样写: static_cast(expression)

 

static_cast 在功能上基本上与C风格的类型转换一样强大,含义也一样。它也有功能上限制。例如,你不能用static_cast象用C风格的类型转换一样把 struct转换成int类型或者把double类型转换成指针类型,另外,static_cast不能从表达式中去除const属性,因为另一个新的类型转换操作符const_cast有这样的功能。

1、用于转换基本类型和具有继承关系的类成员之间转换
2
static_cast不太用于指针类型的之间的转换,它的效率没有reinterpret_cast的效率高。而对于基本类型的转换是完全不行的

 

const_cast 用于类型转换掉表达式的constvolatileness属性。通过使用const_cast,你向人们和编译器强调你通过类型转换想做的只是改变一些东西的constness 或者 volatileness属性。这个含义被编译器所约束。如果你试图使用const_cast来完成修改constness 或者 volatileness属性之外的事情,你的类型转换将被拒绝。

1.用于去除指针变量的常属性,将它转换为一个对应指针类型的普通变量,
2.
反过来也可以将一个非常量指针转换为一个常量指针变量
3.
它无法将一个非指针的常量转换为普通变量

 

第二种特殊的类型转换符是dynamic_cast,它被用于安全地沿着类的继承关系向下进行类型转换。这就是说,你能用dynamic_cast把指向基类的指针或引用转换成指向其派生类或其兄弟类的指针或引用,而且你能知道转换是否成功。失败的转换将返回空指针(当对指针进行类型转换时)或者抛出异常(当对引用进行类型转换时)

 

1.只能在继承类对象的指针之间或引用之间进行类型转换

2.这种转换并非在编译时,而是在运行时,动态的

3.没有继承关系,但被转换的类具有虚函数对象的指针进行转换

4dynamic_cast具有类型检查的功能,在把子类的指针或引用转换成基类时,比static_cast更安全。

 

这四个类型转换符中的最后一个是reinterpret_cast。这个操作符被用于的类型转换的转换结果几乎都是实现时定义(implementation-defined)。因此,使用reinterpret_casts的代码很难移植。

 

reinterpret_cast的最普通的用途就是在函数指针类型之间进行转换。例如,假设你有一个函数指针数组

 

1、将一个类型指针转换为另一个类型指针,这种转换不修改指针变量值数据存放格式
2
、只需在编译时重新解释指针的类型,它可以将指针转化为一个整型数但不能用于非指针的转换

 

小题目,

float a = 1;

int &b = (int &)a;

cout << b << endl;

上述代码的输出是多少?

短路求值

最小化计算(Short-circuit evaluation,也叫做短路计算),是一种计算策略,表达式只有在取得最后值的时候,才会进行计算。这意味着在某些情况下它不需要计算一个表达式的所有部分。

int a = 0;

if (a && myfunc(b)) {

    do_something();

}

在这个例子中,最小化计算使得myfunc(b)永远不会被调用。这是因为 a 等于false,而false AND q无论q是什么总是得到false。 这个特性允许两个有用的编程结构。首先,不论判别式中第一个子判别语句要耗费多昂贵的计算,总是会被执行,若此时求得的值为 false,则第二个子判别运算将不会执行,这可以节省来自第二个语句的昂贵计算。再来,这个结构可由第一个子判别语句来避免第二个判别语句不会导致运行时错误。最小化计算可以避免对空指针进行存取。

namespace

内存分配

一般C/C++程序占用的内存主要分为5

1.   栈区(stack):类似于堆栈,由程序自动创建、自动释放。函数参数、局部变量以及返回点等信息都存于其中。

2.   堆区(heap):使用自由,不需预先确定大小。多数情况下需要由程序员手动申请、释放。如不释放,程序结束后由操作系统垃圾回收机制收回。

3.   全局区/静态区(static):全局变量和静态变量的存储是区域。程序结束后由系统释放。

4.   文字常量区:常量字符串就是放在这里的。程序结束后由系统释放。

5.   程序代码区:既可执行代码。

 

#include <stdio.h>

 

int quanju;/*全局变量,全局区/静态区(static*/

void fun(int f_jubu); /*程序代码区*/

 

int main(void)/**/

{

    int m_jubu;/*栈区(stack*/

    static int m_jingtai;/*静态变量,全局区/静态区(static*/

    char *m_zifum,*m_zifuc = "hello";/*指针本身位于栈。指向字符串"hello",位于文字常量区*/

    void (*pfun)(int); /*栈区(stack*/

    pfun=&fun;

    m_zifum = (char *)malloc(sizeof(char)*10);/*指针内容指向分配空间,位于堆区(heap*/

    pfun(1);

    printf("&quanju   : %x\n",&quanju);

    printf("&m_jubu   : %x\n",&m_jubu);

    printf("&m_jingtai: %x\n",&m_jingtai);

    printf("m_zifuc   : %x\n",m_zifuc);

    printf("&m_zifuc  : %x\n",&m_zifuc);

    printf("m_zifum   : %x\n",m_zifum);

    printf("&m_zifum  : %x\n",&m_zifum);

    printf("pfun      : %x\n",pfun);

    printf("&pfun     : %x\n",&pfun);

    getch();

    return 0;

}

void fun(int f_jubu)    //参数也是放栈上的

{

    static int f_jingtai;

    printf("&f_jingtai: %x\n",&f_jingtai);

    printf("&f_jubu   : %x\n",&f_jubu);/*栈区(stack,但是与主函数中m_jubu位于不同的栈*/

}

//如果函数有返回值,返回值也是放栈上的。

数据对齐

主要是考一些sizeof的题目,有可能会结合虚继承、虚函数一起考。后面在讲虚继承的时候会讲到。

 

Union所有成员都是从低位开始分配内存。

C++对象的初始化顺序

1.         虚基类按深度优先、从左到右的顺序初始化;

2.         基类按在类中声明的顺序初始化;

3.         成员对象按声明的顺序初始化;

4.         自身的构造函数。

 

友元

友元可以定义友元函数、友元类以及友元类成员函数。

class A

{

Int value;

Public:

         friend void f(A& a);

         friend class B;

         friend void C::f();

}

友元的作用在于提高面向对象程序设计的灵活性,是数据保护与对数据存取效率之间的一个折衷方案。友元不具有传递性。

操作符重载

C++中,操作符重载函数可以用两种方式来定义,一种是作为某个类的成员函数,另一种是作为带有类、结构、枚举或它们的引用类型参数的全局函数。一般来说,进行操作符重载时要遵循以下的基本原则,一是要遵循已有操作符的语法,这一点是必须的。二是要尽量遵循已有操作符的语义。三是以下操作符无法重载:. .* :: ?: sizeof

 

为了区分前置及后置的++操作,通过定义一个带有一个int型参数的操作符“++”和“--”的重载函数来解释它们的后置用法。

 

在实现类中的赋值操作符重载的时候,一定要注意是否是自身赋值。

 

newdelete也可以重载

 

类型转换操作符也可以重载,重载的形式为

operator int() {…}

 

函数调用操作符也可以重载。(注意要写两个括号)

void operator()();

 

->重载后,b->action()的调用(假设b是一个指针,指向一个重载了->操作符的类)会首先调用b类里的action方法,若无该方法则调用->操作符重载方法返回后的指针里的相应方法。

虚方法

静态绑定是指在编译期间根据调用者的类型来判断被调用者;动态绑定是指在运行期间根据调用者的类型来确定被调用者。

 

虚函数的动态绑定隐含着:基类中的一个成员函数如果被定义成虚函数,则在派生类中定义的与之具有相同型构的成员函数是对基类该成员函数的重定义(或称覆盖,override)。这里,相同的型构是指派生类中定义的成员函数的名字、参数类型和个数与基类相应成员函数相同,其返回值类型或者与基类成员函数返回值类型相同,或者是基类成员函数返回值类型的派生类。

 

如果基类一个成员函数没有被定义成虚函数,则在派生类中定义的、与之同名(包括同型构)的所有成员函数都不是对基类该成员函数的重定义,它们只是隐藏了基类的同名成员函数。

 

同时需要注意的是,如果派生类的函数与基类的函数同名,但是参数不同。此时,不论有无virtual关键字,基类的函数将被隐藏。

 

一旦在基类中指定某成员函数为虚函数,那么,不管在派生类中是不是给出了virtual声明,派生类(以及派生类的派生类,以此类推)中对其重定义的成员函数均为虚函数。对于虚函数,有以下几点限制:

1.         只有类的成员函数才可以是虚函数;

2.         静态成员函数不能是虚函数;

3.         构造函数不能是虚函数;也不要在构造函数中调用虚函数,这一点实现与Java不同。

4.         析构函数可以(往往)是虚函数。

 

对于每一个类,如果它有虚函数(包括从基类继承来的),则编译程序将会为其创建一个虚函数表(vtbl),表中记录了该类中所有虚函数的入口地址。当创建一个包含有虚函数的类的对象时,在所创建的对象的内存中有一个隐藏的指针(vptr)指向该对象所属类的虚函数表。当通过对象的引用或指针访问类的虚成员函数时,将利用虚函数表来动态绑定实际调用的函数。

 

gcc中派生类与其基类共享虚表指针。

vc中只有在非虚继承的情况下才共享虚表指针,虚继承的情况下不共享虚表指针。

纯虚函数

纯虚函数是指只给出函数声明而没有给出实现(包括在类定义的内部和外部)的虚成员函数,其格式为在函数原型的后面加上符号“=0”。例如:

class A

{

         public:

                   virtual int f()=0;

}

包含纯虚函数的类称为抽象类。抽象类无法实例化。

 

重载与覆盖与隐藏之间的区别

成员函数被重载的特征

1.         相同的范围(在同一个类中);

2.         函数名字相同;

3.         参数不同;

4.         virtual 关键字可有可无。

覆盖是指派生类函数覆盖基类函数,特征是

1.         不同的范围(分别位于派生类与基类);

2.         函数名字相同;

3.         参数相同;

4.         基类函数必须有virtual 关键字。

“隐藏”是指派生类的函数屏蔽了与其同名的基类函数,规则如下

1.         如果派生类的函数与基类的函数同名,但是参数不同。此时,不论有无virtual关键字,基类的函数将被隐藏(注意别与重载混淆)。

2.         如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual 关键字。此时,基类的函数被隐藏(注意别与覆盖混淆)

继承方式

继承方式是指派生类对于基类成员的访问控制。

C++中,派生类拥有基类的所有成员,基类的成员对于派生类的用户的访问控制由基类成员的访问控制和派生类的继承方式共同决定。在定义派生类时需要指出继承方式,继承方式可以是public\private\protected,如果未显式指出,则默认是private。继承方式的含义如下:

1.         public

l  基类的public成员,在派生类中成为public成员

l  基类的protected成员,在派生类中成为protected成员

l  基类的private成员,在派生类中成为不可直接使用的成员

2.         private

l  基类的public成员,在派生类中成为private成员

l  基类的protected成员,在派生类中成为private成员

l  基类的private成员,在派生类中成为不可直接使用的成员

3.         protected

l  基类的public成员,在派生类中成为protected成员

l  基类的protected成员,在派生类中成为protected成员

l  基类的private成员,在派生类中成为不可直接使用的成员

 

同时,子类也可以改变父类中成员的访问控制。只要相应的继承方式中的基类对于子类的访问控制是可见的即可。

例如:

#include <iostream>

using namespace std;

 

class A

{

    void private_hello()

    {

        cout << "hi, world" << endl;

    }

    protected:

        void hello()

        {

            cout << "hello, world" << endl;

        }

};

 

class B: public A

{

    public:

        using A::hello;

        //using A::private_hello; //this is illegal

 

    是否能隐藏

};

 

int main(int argc, char** argv)

{

    B b;

    b.hello(); //ok

 

    return 0;

}

多继承

多继承是指一个类可以有两个或两个以上的直接基类。并且:

1.         继承方式及访问控制的规定同单继承。

2.         派生类拥有所有基类的所有成员。

3.         基类的声明次序决定:

l  对基类构造函数、析构函数的调用次序;

l  对基类数据成员的存储安排。

 

当多个基类中包含同名的成员时,它们在派生类中就会出现名冲突,在子类中调用基类的方法时,需要指明方法所属的基类,如A::f()B::f()。同时也可以使用using A::f的方式在子类中声明使用哪一个基类的相应方法。如:

class A{

         public: void f();

};

class B{

         public: void f();

};

class C: public A, public B{

         public: using A::f;

};

虚继承

在多继承中,如果直接基类有公共的基类,则会出现重复继承,这样公共基类中的数据成员在多继承的派生类中就有多个拷贝。通常情况下,需要将这多个拷贝合而为一,这时就应使用虚基类。对于虚基类,应该注意以下两点:

1.         虚基类的构造函数由最新派生出的类的构造函数调用。

2.         虚基类的构造函数优先于非虚基类的构造函数执行。

 

赋值操作符重载是不被继承的。

模板

模板是一段程序代码,它带有类型参数,在程序中可以通过给这些参数提供一些类型来得到针对不同类型的具体代码。函数模板是指带有类型参数的函数定义,其格式如下:

Template <class T1, class T2, …>

<返回值类型> <函数名>(<参数表>)

{

}

 

除了类型参数外,模板也可以带有非类型参数。例如:

template<class T, int size>

 

如果一个类的成员类型可变,则该类称为类属类。在C++中,类属类一般用类类模板实现。类模板指带有类型参数的类定义,其格式为:

Template <class T1, class T2, …>

class <类名>

{};

 

一个模板代表了一组函数或类,它实现了一种代码复用。在使用模板时首先要实例化。函数模板的实例化可以是隐式的,也可以是显式的;而类模板的实例化则是显式进行的。

 

一个模板可以有很多实例,但是,是否实例化该模板的某个实例要由使用情况来决定。在C++中,由于源文件(模块)是分别编译的,如果在一个源文件中定义和实现了一个模板,但在该源文件中未使用到该模板的某个实例,则在相应的目标文件中,编译程序不会生成该模板相应实例的代码。

 

声明类型时可以用class也可以用typename,这二者在这里是没有任何区别的。有些人只是习惯在使用类变量的时候用class使用普通类型变量的时候用typename,但这不并是强制的。同时,typename还有另一个功能,就是告诉编译器其后跟着一个类型。例如

template<class C>

C::iterator *i;

编译器无法区分C::iterator是个静态变量还是个类型(一个是做乘法一个是声明指针),但是可以用typename C::iterator *i来声明。

你可能感兴趣的:(C++,c,总结,回顾)