C/C++学习总结(复习)

C/C++学习了太久,有些知识点有些模糊了,花了半个多月,重新整理了一些,自认为比较重要的知识点,主要是用于自己学习。


1.volatile优化总结:

volatile 影响编译器编译的结果,指出,volatile 变量是随时可能发生变化的,与volatile变量有关的运算,不要进行编译优化,以免出错

个定义为volatile的变量是说这变量可能会被意想不到地改变,这样,编译器就不会去假设这个变量的值了。精确地说就是,优化器在用到这个变量时必须每次都小心地重新读取这个变量的值,而不是使用保存在寄存器里的备份。

例如多线程程序共享一个变量,它可能会被其他线程更改。

2.Volatile功能总结:

a) 告诉编译器不做任何优化

例如:int a;

a = 1;

a = 2;编译器可能会省略a=1,直接优化为a=2

 

b) 表示用volatile定义的变量会在程序外被改变,每次都必须从内存中读取,而不能把他放在cache或寄存器中重复使用

 

2.malloc 与 calloc的区别:

void *malloc( size_t size ); //分配的大小

void *calloc( size_t numElements, size_t sizeOfElement ); // 第一个参数为元素的个数,第二个参数是每个元素的大小

不同点是:用malloc分配存储空间时,必须由我们计算需要的字节数。如果想要分配5int型的空间,那就是说需要5*sizeof(int)的内存空间:最大的区别就是:用malloc只分配空间不初始化,也就是依然保留着这段内存里的数据,而calloc则进行了初始化,calloc分配的空间全部初始化为0,这样就避免了可能的一些数据错误。

3.//关于std::stack的用法

栈少不了的三个核心接口:

1)void push() 插入元素到栈顶

2)void pop() 移除栈顶元素(注意,函数类型为 void)

3)value_type& top() 返回栈顶元素,并不会移除这个元素(注意,返回的是栈顶元素的引用)

4)empty();是否为空

 

4.虚析构

我们知道,用C++开发的时候,用来做基类的类的析构函数一般都是虚函数。可是,为什么要这样做呢?下面用一个小例子来说明:    
    有下面的两个类:

class ClxBase
{
public:
    ClxBase() {};
    virtual ~ClxBase() {};

    virtual void DoSomething() { cout << "Do something in class ClxBase!" << endl; };
};

class ClxDerived : public ClxBase
{
public:
    ClxDerived() {};
    ~ClxDerived() { cout << "Output from the destructor of class ClxDerived!" << endl; }; 

    void DoSomething() { cout << "Do something in class ClxDerived!" << endl; };
};

    代码

ClxBase *pTest = new ClxDerived;
pTest->DoSomething();
delete pTest;

    的输出结果是:

Do something in class ClxDerived!
Output from the destructor of class ClxDerived!

    这个很简单,非常好理解。
    但是,如果把类ClxBase析构函数前的virtual去掉,那输出结果就是下面的样子了:

Do something in class ClxDerived!

也就是说,类ClxDerived析构函数根本没有被调用!一般情况下类的析构函数里面都是释放内存资源,而析构函数不被调用的话就会造成内存泄漏。我想所有的C++程序员都知道这样的危险性。当然,如果在析构函数中做了其他工作的话,那你的所有努力也都是白费力气。
    所以,文章开头的那个问题的答案就是--这样做是为了当用一个基类的指针删除一个派生类的对象时,派生类的析构函数会被调用。
    当然,并不是要把所有类的析构函数都写成虚函数。因为当类里面有虚函数的时候,编译器会给类添加一个虚函数表,里面来存放虚函数指针,这样就会增加类的存储空间。所以,只有当一个类被用来作为基类的时候,才把析构函数写成虚函数。

5.虚析构(2

虚析构函数
析构函数的工作方式是:最底层的派生类(most derived class)的析构函数最先被调用,然后调用每一个基类的析构函数。

因为在C++中,当一个派生类对象通过使用一个基类指针删除,而这个基类有一个非虚的析构函数,则结果是未定义的。运行时比较有代表性的后果是对象的派生部分不会被销毁。然而,基类部分很可能已被销毁,这就导致了一个古怪的“部分析构”对象,这是一个泄漏资源。排除这个问题非常简单:给基类一个虚析构函数。于是,删除一个派生类对象的时候就有了你所期望的正确行为。将销毁整个对象,包括全部的派生类部分。

但是,一般如果不做基类的类的析构函数一般不声明为虚函数,因为虚函数的实现要求对象携带额外的信息,这些信息用于在运行时确定该对象应该调用哪一个虚函数。典型情况下,这一信息具有一种被称为 vptr(virtual table pointer,虚函数表指针)的指针的形式。vptr 指向一个被称为 vtbl(virtual table,虚函数表)的函数指针数组,每一个包含虚函数的类都关联到 vtbl。当一个对象调用了虚函数,实际的被调用函数通过下面的步骤确定:找到对象的 vptr 指向的 vtbl,然后在 vtbl 中寻找合适的函数指针。这样子会使类所占用的内存增加。

6.const 修饰类成员函数

如果一个成员函数的不会修改数据成员,那么最好将其声明为const,因为const成员函数中不允许对数据成员进行修改,如果修改,编译器将报错,这大 大提高了程序的健壮性

7.Mutable中文意思是“可变的,易变的”,跟constant(既C++中的const)是反义词。
  在C++中,mutable也是为了突破const的限制而设置的。被mutable修饰的变量,将永远处于可变的状态,即使在一个const函数中。
  我们知道,如果类的成员函数不会改变对象的状态,那么这个成员函数一般会声明成const的。但是,有些时候,我们需要在const的函数里面修改一些跟类状态无关的数据成员,那么这个数据成员就应该被mutalbe来修饰。

8.指针与引用的区别

指针是变量地址,间接对变量操作。引用相当于是变量的别名,直接对变量本身操作。指针间接操作需要额外的空间存放指针,而引用不需要所以更节省空间。

9.关于引用的总结

1引用变量:
1)引用的内部实现为相当于一个指针变量,与指针的实现方式类似;
2)引用变量内存单元保存的指向变量地址(初始化时赋值),与指针不同地方时,引用变量在定义时必须初始化,而且使用过程中,引用变量保存的内存单元地址值是不能改变的(这一点通过编译器来实现保证);
3)引用也可以进行取地址操作,但是取地址操作返回的不是引用变量所在的内存单元地址,而是被引用变量本身所在的内存单元地址;
4)引用的使用,在源代码级相当于普通的变量一样使用,但在函数参数传递引用变量时,内部传递的实际是变量的地址值(这种机制的实现是通过编译器(编译手段)来实现的)。

10.c++不能重载的运算符

c++不能重载的运算符有.(点号),::(域解析符),?:(条件语句运算符),sizeof(求字节运算符),typeid,static_cast,dynamic_cast,interpret_cast(三类类型转换符)。

 

11.继承

子类公有(public)继承父类,所以子类可以通过对象访问父类的公有成员函数,由于调用的是父类的公有成员函数(该函数中的this指针存放的是父类对象的地址),所以打印的是父类A的数据,即便子类与父类的数据同名。

 

12.c++四种强制类型转换

(1)dynamic_cast

dynamic_cast只能将指向派生类对象的基类指针或引用转换为派生类的指针或引用,若用于其他转换则指针为空,引用则抛出异常。此为向下类型转换。

      dynamic_cast转换符只能用于指针或者引用。dynamic_cast转换符只能用于含有虚函数的类。dynamic_cast转换操作符在执行类型转换时首先将检查能否成功转换,如果能成功转换则转换之,如果转换失败,如果是指针则反回一个0值,如果是转换的是引用,则抛出一个bad_cast异常,所以在使用dynamic_cast转换之间应使用if语句对其转换成功与否进行测试,比如pd=dynamic_cast<D*>(pb); if(pd){…}else{…},或者try{};catch(bad_cast){}

该操作符用于运行时检查该转换是否类型安全,但只在多态类型时合法,即该类至少具有一个虚拟方法。dynamic_caststatic_cast具有相同的基本语法,dynamic_cast主要用于类层次间的上行转换和下行转换,还可以用于类之间的交叉转换。在类层次间进行上行转换时,dynamic_caststatic_cast的效果是一样的;在进行下行转换时,dynamic_cast具有类型检查的功能,比static_cast更安全

 

2static_cast

最常用的类型转换符,在正常状况下的类型转换,如把int转换为float,如:int ifloat f; f=floati;或者f=static_cast<float>(i);

3const_cast

用于取出const属性,把const类型的指针变为非const类型的指针,如:const int *fun(int x,int y){}int *ptr=const_cast<int *>(fun(2.3))

4reinterpret_cast

interpret是解释的意思,reinterpret即为重新解释,此标识符的意思即为数据的二进制形式重新解释,但是不改变其值。如:int i; char *ptr="hello freind!"; i=reinterpret_cast<int>(ptr);这个转换方式很少使用。

 

13.区分 浅/深拷贝操作 和 赋值操作:

 

14.using 访问名称空间std

(1)使用using namespace std;放在函数定义之前,让整个文件都使用。

(2)使用using namespace std;放在函数定义之内,让该函数使用。

(3)使用using std::cout,使用指定元素。

(4)不使用using命令,直接使用std::前缀。

 

15.C++命名规则

(1)在名字中只能使用数字,字母和下划线。

(2)第一个字符不能是数字。

(3)区分大小写字符。

(4)不能使用c++关键字。

16.各类型最小长度

(1)short最少16位。

(2)Int至少与short一样长。

(3)Long至少32位,且至少与int一样长.

(4)Long long至少64位,且至少与long一样长。

17.C++初始化的方式

(1)int a = 10;

(2)Int a(10);

(3)Int a{10};

(4)Int a = {10};

(5)Int a = {};//当大括号中没有任何内容时,默认初始化为0.

 

18.C++如何确定常量类型

首先看后缀。后缀是放在数字常量后面的字母,用于表示类型。Ll表示该整数为long型,uU表示为unsigned int型常量,ul表示为unsigned long型。默认的十进制数将使用能够存储该数的最小类型来表示。十六进制通常使用unsigned int 来表示。

 

19.单引号双引号等特殊字符

使用转义字符来表示。

20.换行三种方式

Cout<<endl;

Cout<<\n;

Cout<<\n;

21.使用八进制,十六进制转义

例如:\nASCII码值为12.所以可以用\012来表示\n,还可以用0xA来表示。

22.char有无符号

Char在默认情况下既不是有符号,也不是无符号,是否有符号由编译器来决定。如果有无符号区别特别重要,可以通过signed charunsigned char来显示的指定。

23.宽字符类型(wchar_t)

宽字符串使用L前缀。

24.const声明

在使用const限定符声明常量时未赋值,则该常量的值是未确定的,且无法修改。

25.使用const定义常量与define定义相比的优势

a) Const可以明确指定类型。

b) 可以使用c++的作用域规则将定义限制在特定的函数或文件中。

26.浮点类型

浮点类型保存是有一个基准数和一个缩放因子。

27.浮点类型的表示法

a) 普通小数点式表示方法

b) E表示法,E代表10的几次方。

28.三种浮点数类型

a) Float

b) Double

c) Long double

d) Float至少32位,double至少48位,且不少于floatlong double至少和double一样多。通常float32位,double64位,long double80,96128位。

e) Float至少有6位有效位,double至少有13位是精确的,系统确保15位有效位。

29.浮点型常量

a) 默认都是double型的。

b) f后缀表示为float型。

c) l后缀表示为long double型。

30.浮点型的缺点(精度问题)

程序如下:

Float a = 2.34E+22f;

Float b = a+1.0f;

Cout<<a;

Cout<<a-b<<endl;

结果分析:

a-b = 0;因为afloat型的,只能保持数字的前六位或者前七位,而b对第23位加1,因此修改完之后对这个数并没有什么影响,后面的精度已经被丢失了。

31.%运算符

a) 操作数只能是整数。

b) 正负号与被除数保持一致。

32.概述c++自动执行类型转换的情况:

a) 将一种算数类型的值赋值给另一种类型的值时,c++对值进行转换。

b) 表达式中包含不同类型时,C++将对值进行类型转换。

c) 将参数传递给函数时,C++将进行类型转换。C++函数将值返回时,也会进行类型转换。

33.初始化和赋值进行的转换

a) C++允许将一种类型的值赋给另一种类型的变量,这样做的话,类型将被转化为接受类型的变量。

b) 将一个值赋值给另一个取值更大的类型,通常不会有问题。而将大值赋值给小值通常会有潜在的问题。

i. 将较大的浮点类型转换为较小的浮点类型,精度会有所降低,而且值可能超出目标的取值范围,在这种情况下结果将是未知的。

ii. 将浮点类型转换为整型。会导致小数部分丢失,原来的值可能超出目标类型的取值范围,结果也将是未知的。

iii. 将较大的整型转换为较小的整型,如long变为short。原来的值超出目标类型的取值范围,通常只复制右边字节。

34.{}方式初始化时进行的转换

a) 使用大括号初始化的方式叫做列表初始化,它主要用于对复杂数据进行初始化,所以对类型转换的要求更严格。

b) 列表初始化时不允许窄缩,即变量的类型可能无法表示赋给他的值。例如:不允许将浮点型转换为整型。在不同整型之间转换或者整型转换为浮点类型可能是被允许的,前提是编译器知道目标变量可以正确的存储给他的值。例如可使用int变量初始化long型变量,因为long至少与int一样长;int存储long型变量也是有可能的,只要int能够存储给它的long常量。

i. 例如:

1. Const int code = 66;

2. Int x = 66;

3. Char c1{31325};

4. Char c2 = {66};

5. Char c3{code};

6. Char c4 = {x};

7. X = 31325;

8. Char c5 = x;

分析:12可以。3数太大不可以。Code是常量,c2可以存储它的值,所以45可以。6不可以,因为x是变量,大小对编译器来说未知。8可以,因为没有初始化列表那么严格。

 

 

 

35.表达式中的转换。

a) 一些类型在出现时就会自动转换。

i. 在计算表达式时,c++boolcharunsigned charsigned charshort值转换为int。这些转换被称为整型提升。

例:short a = 20;

Short b = 30;

Short c = a+b;

第三步准确分析过程应该是,获取ab的值将它们转换为int,最后将这个int的结果转换为short保存。

b)编译器通过校验表来确定在表达式中的类型转换

(1)如果有一个操作数是long double,则另一个数被转化为long double

(2)否则,有一个数是double,则另一个被转换为double

(3)否则有一个操作数是float,则另一个是float

(4)否则,说明操作数是整数,则进行整型提升。

(5)在这种情况下,如果两个操作数都是有符号或者无符号的,则将级别低的操作数转换为高级版操作数。

(6)如果一个操作数是有符号的,另一个操作数是无符号的,且无符号操作数级别较高,则将有符号操作数转换为无符号所属类型。

(7)否则,如果有符号类型可表示所有无符号类型所有的可能取值,则将无符号操作数转换为有符号操作数。

(8)否则,将两个操作数都转化为有符号类型的无符号版本。

(9)整数级别的概念:有符号整型级别从高到低依次是long longlongintshortsigned char。无符号与有符号相同。类型charunsigned charsigned char的级别相同。类型bool的级别最低。

36.强制类型转换

a) (long)a;

b) Long(a);

c) 以上两种都可以。

37.auto关键字的新功能

a) 可以让编译器根据初始值的类型判断变量的类型,而不指定变量的类型、

i. Auto n = 100;//n is int

ii. 处理复杂类型时,自动推断功能强大。

1. Std::vector(double) scores;

2. Std::vector(double)::iterator pv = score.begin();

可以写成如下形式:

3. Std::vector(double) scores;

4. Auto pv = scores.begin();

 

38.大端存储,小端存储

小端:较高的有效字节存放在较高的的存储器地址,较低的有效字节存放在较低的存储器地址。
大端:较高的有效字节存放在较低的存储器地址,较低的有效字节存放在较高的存储器地址。

39.数组部分初始化

如果只对数组部分初始化,其余部分元素设置为0。如果初始化为{1}而不是{0},则第一个元素赋值为1,其余仍然为0

40.字符串与字符常量呼唤

Char c = abc;

只是将abc所在的内存地址,部分赋值给字符c

41.读取一行

String str;

Getline(cin,str);

42.结构体初始化

支持列表初始化,并且(=)号是可选的。如果{}没有参数,就默认为0.

最后,不允许缩窄操作。

43.结构体成员赋值

可以使用赋值运算符将结构赋值给另一个同一类型的结构,这样结构中的每个成员都将被设置为另一个结构相应成员的值,即使成员是数组。

44.union共用体

创建方式:

Union name

{

Int id;

Char c_id[20];};

45.匿名共用体

Struct haha

{

Union 

{

Long id;

Char str_id[20];

};

};

由于共用体匿名,所以里面的两个变量都视为结构体haha的成员。

46.枚举

a) 创建枚举

i. Enum Color{RED,BLUE};

ii. Color成为新类型的名称

iii. 在不进行强制转换的情况下,只能讲定义枚举时使用的枚举量赋给这种枚举类型。例Color c = (Color)3;

iv. 如果打算只使用常量,而不创建枚举类型的变量,则可以省略枚举类型的名称。

47.指针和数字

要将数字值作为地址来使用,应通过强制类型转换,将数字转换为适当的地址类型。

例:int *pt = (int *)0xB8000000;

48.Delete

不要尝试释放已经释放的内存,不能用delete释放变量声明得到的内存。

然而,对空指针使用delete是安全的。

49.使用new创建动态数组。

Int *p = new int[10];

对于new创建的数组,应当使用delete [] p;这种形式来释放。

方括号告诉delete,应该释放整个数组,而不仅仅是指针指向的元素。

50.使用newdelete时应该遵守的守则

a) 不要使用delete释放不是new分配的内存

b) 不要重复delete一块内存多次

c) 如果使用new[]为数组分配内存,则应该使用delete[]来释放。

d) 如果使用new[]为一个实体分配内存,则应该使用delete(没有方括号)来释放。

e) 对空指针应用delete是安全的。

51.指针和数组名的区别

P = p+1;

//p是指针,则表达式是正确的。若p是数组名,表达式错误。数组名是常量,数组名的值无法修改,但是指针是变量则可以修改。

52.数组的地址

数组名被解释为数组的第一个元素的地址,对数组名取地址,得到的是整个数组的地址。

从数字上说,两个地址的值相同,但是意义上并不相同。间接引用的范围也不同。

53.指针数组和数组指针

Int* p[10];//指针数组

Int (*p)[10];//数组指针。

54.数组的静态联编和动态联编

a) 数组声明来创建数组时,是静态联编,数组的长度在编译时设置。

b) 使用new[]创建数组时,即采用动态联编的方式,即在运行时为数组分配空间。

55.c++三种内存数据管理的方式

a) 自动存储

b) 静态存储

c) 动态存储

56.for规则修改

a) 可以在for循环中直接定义变量,这种变量只存在于for语句中,也就是离开了循环,这种变量将消失。

b) 但是仍有些编译器,将for循环中定义的变量视为整个函数定义的局部变量,离开for循环后,变量可以正常使用。

57.副作用和顺序点

a) 副作用指的是在计算表达式时对某些东西(如存储在变量中的值)进行了修改。

b) 顺序点:在这里,进入下一步之前将确保对所有的副作用都进行了评估。例如分号就是一个顺序点,这意味着在处理下一条语句之前,赋值运算符、递增运算符和递减运算符执行的所有修改都必须完成。

c) 例:y=(4+x++)+(6+x++);在上例中,4+x++并不是一个完整的表达式,因此c++不保证x的值在计算子表达式4+x++x的值立即加1,它只保证整个表达式完成后x的值加2.

58.++++效率问题

a) 对于内置的类型,效率并不会有差别。

b) 对于用户定义类型,后++需要一个临时变量,并将临时变量的值返回,所以前++的效率更高。

59.逗号运算符

a) 逗号运算符是一个顺序点,它会先计算第一个表达式,再计算第二个表达式。例:i=20,j=2*i;//i20j结果是40.

b) C++规定,逗号表达式的值是第二部分的值。

c) 在所有运算符中,逗号运算符的优先级是最低的。

60.字符串

数组名是数组的首元素的地址,用引号括起来的字符串常量也是其地址。

61.空测试条件的for循环

空测试条件的for循环被视为true

For(;;);//死循环

62.for循环的新特性//类似于foreach

Double x[10];

Double y;

For(y:x)

{}

但是,以上可以遍历的是每个元素的值,如果想遍历并修改元素,需要使用引用。

Double &y;

63.switch()case

Switch里必须结果是整数值的表达式。另外,每个标签都必须是整数常量表达式。最常见的标签是intchar常量,也可以是枚举量。

64.函数的返回值

a) 对于有返回值的函数,必须使用返回语句,以便将值返回给调用函数。

b) 值本身可以是常量,变量,也可以是表达式,只是其结果的类型必须为函数原型里声明的返回类型或者可以被转换为返回类型。

 

65.函数的调用过程分析(以ARM内核为例剖析)

例:

Int main(void)//1

{//2

int k,i=1,j=2;//3

addsub(i,j);//4

k = 3;//5

}//6

Int addsub(int a,int b)//7

{//8

Int c;//9

c = a+b;//10

return c;//11

}//12

分析:对于上面的程序,编译器会将第4行翻译为指令BL addsub,将第11行编译为指令MOV pc,lr;

在这里,BL addsub会完成两件事:

(1)将子程序的返回地址,也就是第5行代码在内存中的位置保存到寄存器LR(链接寄存器,存放子程序的返回地址)中。

(2)跳转到子程序addsub处开始执行。

这样就完成了子程序调用。

而指令MOV pc,lr;将保存在lr中的返回地址放回PC寄存器中,完成了子程序返回。

66.为什么需要声明函数原型

a) 告诉编译器返回值的类型,当返回值被保存在内存中时,编译器知道应该以多少字节获取它。

b) 没有原型,编译器自己去文件找定义的话,效率不高。

67.const与指针

a) 四种情况。将常规变量地址赋给常规指针,将常规变量地址赋给指向const的指针,将const变量的地址赋给指向const的指针,将const变量的地址赋给常规指针。最后一种情况是不可行的,编译器会报错。如果非要这么做,可以使用const_cast强制转换。

b) Const int *p;是指p指向的是一个常量,也就是*p的值不可以修改。

c) Int const *p也是指p指向的是一个常量,*p的值不可以修改。

d) Int * const p//指的是p本身是个常量,这个指针的值不可以修改。

68.应该尽可能的使用const

a) 这样可以避免无意修改数据导致的编程错误

b) 使用const使得函数能接受const或者非const实参,否则只能接受非const数据。

69.指针数组与指向数组的指针

Int *p[10];这是一个指针数组,数组里所有的元素都是int*的指针。

Int (*p)[10];这是一个数组指针,指向int型数组的指针。

70.二维数组名作为参数传递到函数

使用数组指针来接受参数。

例:int a[3][4];

使用int (*p)[4]来接受。可将二维数组看做是元素为int[4]型的一维数组。

71.函数地址

函数名就是函数的地址。

72.函数指针

对于函数指针pf(*pf)pf等价,也就是pf()或者(*pf)()都可以完成函数调用。

73.创建函数指针的数组

例函数原型为:

Const double* f1(const double*,int);

则函数指针数组为

Const double*(*f2[3])(const double*,int);

则指向函数指针数组的指针为:

Const double*(*(*p1)[3])(const double*,int);

从上式可以看出,定义很复杂,所以可以使用auto

Auto p = &f2;//即可完成指向函数指针数组的指针的定义。

74.内联函数

内联函数的编译代码与其他程序代码内联起来。也就是说编译器将使用相应的函数代码替换函数调用。

内联函数的特殊规则要求在每个使用它们的文件中都对其进行定义。确保内联的定义对多文件程序中的所有文件都可以用,最简单的方法就是:将内联定义放在头文件中。

 

75.使用内联函数,必须采取下述措施之一

a) 在函数声明前加上关键字inline

b) 在函数定义前加上关键字inline

通常做法是省略原型,将整个定义放在本应该提供原型的地方。

76.内联不一定都能实现

a) 程序猿请求函数作为内联函数,编译器并不一定会满足这个要求

b) 函数体过大,可能不会作为内联函数

c) 函数中有递归调用,内联函数不能递归。

d) 虚函数不能是内联函数

77.内联函数参数传递

内联函数和常规函数一样,也是按值传递参数的,如果是表达式的话,内联函数会将表达式的值传递进去,而宏只会进行简单的文本替换。

78.内联与宏的区别

区别如下:

1)内联在编绎时展开,宏在预编译时展开。 展开的时间不同。

2)编译内联函数可以嵌入到目标代码,宏只是简单文本替换。

3)内联会做类型,语法检查,而宏不具这样功能。

4)宏不是函数,inline函数是函数

5)宏定义小心处理宏参数(一般参数要括号起来),否则易出现二义性,而内联定义不会出现。

79.内联替代宏的优势

a) 内联函数在运行时可调试,而宏不可以

b) 编译器会对内联函数的参数类型做安全检查或自动类型转换,而宏不会

c) 内联函数可以访问类的成员变量,而宏不可以

d) 在类中声明同时定义函数,会自动转化为内联函数

80.Inline应该放在那里的争议

林锐认为inline关键字应该放在函数定义前才能有效,仅将inline放在函数声明前不起任何作用。

c++primer plus解释为放在声明或者定义任意处都可以。

c++ primer解释也是必须放在实现那里。

81.引用

引用创建时,必须进行初始化。

int x=50,y=100;

Int &a = x;

a = y;//并不再是用a引用y,而是将y的值赋给变量x.

82.形参为引用和形参为值的函数不能重载,编译器并不能区分两者。

83.当引用作为函数参数(重点)

a) 将表达式作为参数传递,对于现代c++这是错误的。但是有些情况下,编译器将为表达式的结果创建一个临时变量,然后函数参数将成为这个临时变量的引用。

b) 那么编译器将在何时创建临时变量(如果引用参数是const的)

i. 实参的类型和形参一致,但不是左值

ii. 实参的类型不正确,但可以转换为正确的类型。

84.引用不占用内存单元

a) 声明一个引用不是新定义一个内存变量,它只是表明该引用名是目标变量的一个别名。

b) 因此,引用本身不占内存单元,系统也不给引用分配内存单元。

c) 所以,对引用取地址,就是对目标变量取地址,两种获得地址相同。

d) 不能建立对数组的引用。数组是若干元素的集合,所以无法建立。

e) 这也是使用引用与使用指针相比的优势所在,节省内存。

85.引用作为函数参数应尽量声明为const的理由

a) 使用const可避免无意中修改数据引发编程错误

b) 使用const能使函数处理const与非const的实参,否则只能接受非const数据

c) 使用const引用能够正确生成并使用临时变量

86.使用引用的好处

与按值传递相比,节省了往临时对象拷贝数据的过程,节省了时间与空间。

87.关于常引用的问题

String foo();

Void bar(string& s);

那么下面表达式是非法的:

Bar(foo());

Bar(hehe);

资料中解释的原因在于,它们都会生成临时对象,而在c++中临时对象都是const类型的。因此上式试图将一个const实参转换为非const类型,所以非法。

我认为c++中临时对象都是const类型的是存在争议的。在c++ primer plus中有一下说法:

编译器将在何时创建临时变量(如果引用参数是const的)

iii. 实参的类型和形参一致,但不是左值

iv. 实参的类型不正确,但可以转换为正确的类型。

它的意思是,必须以形参为const为前提,才有临时对象的生成。

总而言之,非常量引用的初始化必须是左值。而常量引用可以的初始化可以是常量。

88.关于c++中赋值运算符重载的小技巧

重载c++中的=运算符时,应该返回的是调用者的引用。因为在c++中,可以连续赋值。

比如(x=10)=100;是正确的,而在C中是不可以的。对于这种可连续使用的运算符都重载都应该返回引用,再比如<<运算符。

89.引用使用多态

父类的类型的引用去引用子类对象,或者父类的指针指向子类的对象都可以实现多态。

90.默认参数函数

对于带参数列表的函数,必须从右向左添加默认值。也就是说,要为某个参数设置默认值,必须为它右边的所有参数提供默认值。

 

91.函数重载

一般认为是函数的形参的类型或者数量不同,函数名相同的函数是重载函数。如果仅仅是返回值类型不同,并不是重载。

92.类中的一个特殊重载

在类中会有这样一种重载,它是合法的。

Class A {

int function ();

int function () const;

};

可以看到在A类中,function函数是发生重载了,而且是合法的。而且在调用时,只用A类的const对象才能调用const版本的function函数,而非const对象可以调用任意一种,通常非const对象调用不是const版本的function函数。

原因是:按照函数重载的定义,函数名相同而形参表有本质不同的函数称为重载。在类中,由于隐含的this形参的存在,const版本的 function函数使得作为形参的this指针的类型变为指向const对象的指针,而非const版本的使得作为形参的this指针就是正常版本的指 针。此处是发生重载的本质。重载函数在最佳匹配过程中,对于const对象调用的就选取const版本的成员函数,而普通的对象调用就选取非const版 本的成员函数。

(注:this指针是一个const指针,地址不能改,但能改变其指向的对象或者变量)

 

93.不要将变量声明或函数定义放到头文件中

如果在头文件包含一个函数的定义,然后在其他文件中多次包含该头文件,那么会导致同一个函数重定义,除非是内联函数

94.register关键字在c++11中已经失去了作用,保留它只是为了与之前的程序兼容

95.三种静态持续变量

a) 要创建链接性为外部的静态持续变量,必须在代码块的外面声明它。

b) 要创建链接性为内部的静态持续变量,必须在代码块的外面声明它,并使用static限定符。

c) 要创建没有链接性的静态持续变量,必须在代码块的内部声明它,并使用static限定符。

96.静态持续变量的初始化特征

未被初始化的静态变量的所有位都被设置为0,这种变量被称为零初始化的。

97.c++单定义规则

a) 一方面,在每个使用外部变量的文件中,都必须声明它。

b) 另一方面,单定义规则指出,变量只能有一次定义。为满足这种需求,C++提供了两种变量声明。

i. 一种是定义声明,简称定义,它给变量分配存储空间。

ii. 另一种是引用声明,简称声明,它不给变量分配存储空间,因为它引用已有的变量。

iii. 引用声明使用关键字extern,且不进行初始化;否则声明为定义,导致分配存储空间。

iv. 如果要在多个文件中使用外部变量,只需在一个文件中包含该变量的定义,但在使用改变量的其他所有文件中,都必须使用extern声明它,否则会重定义。

98.访问与内部变量同名的外部变量

a) 在函数内,使用extern关键字重新声明要使用的函数外的变量。

b) 使用作用域运算符,在变量名前面加上::,表明要使用全局变量

 

99.对于静态变量

只会初始化一次,后面可以重新赋值,但是初始化将会失效。只有未使用extern的变量才可以初始化。

100.const对全局静态变量的影响

a) 定义的全局变量默认的链接性是外部的,而const全局变量的链接性是内部的。因此,可以将const修饰的全局变量

例如const int a = 10;初始化后放到头文件中,而不会发生重定义。

b) 如果需要将const常量链接性改为外部的,那么加上extern关键字就行了

例如:extern const int a = 10;这与常规外部变量不同,常规外部变量在定义时默认就是extern的,不用特意加extern关键字,而外部常量必须加extern,并且其他文件想使用,也必须用extern引用声明,否则会出现重定义的错误。

101.使用namespace创建名称空间

注意:记住名称空间的定义最后不需要分号。

102.名称空间可以使全局的,也可以位于另一个名称空间中,但不能位于代码块中,即函数或类内部。因此在默认情况下,在名称空间中声明的名称的链接性为外部的(除非它引用了常量)

103.命名空间可以是不连续的,即根据需求分散定义在不同的文件中。如果所定义的namespace不是引用前面定义的命名空间,那么就使用这个名字创建新的命名空间,否则,这个定义打开一个已经存在的命名空间,并将新的内容添加进去。

104.命名空间定义了函数的声明,那么它的定义既可以实现在命名空间内部,也可以在外部实现,但是需要name::fun()去标示,就类似于在类外实现成员函数一样。

105.全局命名空间

定义在全局作用域的名字(全局变量,函数等),默认都是定义在全局命名空间中的。全局命名空间是隐式声明的,没有名字,可以通过::member_name引用全局命名空间中的成员。

106.名称空间可嵌套定义

Namespace A

{

Namespace B

{

 

}

}

 

 

107.名称空间起别名

假设有命名空间A

Namespace A

{}

则可用下面语句让B成为A的别名

Namespace B = A;这个技术主要是为了简化对多次嵌套的命名空间的使用。

108.同一个类的所有对象共享同一代码段

109.类是一种抽象的数据类型,是将数据与对数据的操作封装在一起的集合体。

110.类中的数据成员类型

数据成员的类型可以是任意的,可以是普通类型,也可以是其他类。另一个类的对象,可以做该类的成员,但是自身的类对象不可以,而自身类的引用或者指针又是可以的。

111.三种调用构造函数初始化对象的方式

a) A a = A();显示调用构造函数

b) A a();隐式调用构造函数

c) A* p = new A()

112.每个类只有一个析构函数和一个赋值函数,但可以有多个构造函数(包括一个拷贝构造函数,其他为普通构造函数)。

对于任意类,如果不想编写上述函数,编译器会自动生成四个缺省函数。

(1)缺省的无参构造函数

(2)缺省的拷贝构造函数

(3)缺省的析构函数

(4)缺省的赋值函数 A& operate=(const A&a);

113.缺省的赋值函数和缺省的拷贝构造函数,采用的都是浅拷贝,当类中有指针时,就会产生问题。

114.默认构造函数

默认构造指的是在没有提供显示的初始值时,用来创建对象的构造函数。

例如:A a;时调用默认构造,或者A*a = new A;

默认构造函数没有参数。

奇怪的是,当且仅当没有定义任何构造函数时,编译器才会提供默认构造函数。为类定义了构造函数后,程序猿必须为它提供默认的构造函数。如果提供了非默认构造函数,但是没有提供默认构造函数,那么下面的定义将会出错。A a;因为他找不到隐式的默认构造。

115.定义默认构造的两种方式

a) 一种是给已有构造函数的所有参数提供默认值

b) 直接创建一个没有任何参数的构造函数

 

 

116.何时调用析构函数

a) 析构函数或者构造都可以显示的调用,但通常不那么做

b) 如果创建的是静态存储类对象,则析构将在程序结束时自动调用。

c) 如果创建的是自动存储类对象,则析构将会在代码块执行完毕后调用。

d) 如果对象是通过new创建的,则它将驻留在栈内存或自由存储区中,当使用delete释放。

117.为什么构造函数和析构函数没有返回值

假设有以下对象创建

A a = A();这是通过显示调用构造创建对象,如果构造可以返回值

Int A(){return 1}

那么A()会返回1,A a = 1;由于赋值函数有默认函数,并且形参为常引用,所以会调用类A的有参构造函数创建一个临时对象,然后赋值给a。总之这会非常混乱。

 

而最关键的是,如果它们有返回值,要么编译器知道如何处理返回值,要么就只能由程序猿去显示的调用它们,这么一来,安全性就被破坏了。

118. C++的访问修饰符的作用是以类为单位,而不是以对象为单位

所以,在类的成员函数内可以访问同种对象的私有成员(同种类则是友元关系),所以拷贝构造函数里,可以直接访问另外一个同类对象(引用)的私有成员。

119.关于new和delete,new[]和delete[]的完全理解

new的过程:1.先分配空间。2.然后执行构造。3.将新对象的空间地址返回

new[]与new的区别:new[]会在对象数组前面多分配4个字节,保存数组长度,对于普通类型,好像并不会。然后,将移动4个字节后的地址,作为可用首地址返回给程序。也就是说,调用new[]返回的地址,并不是真正的完全的分配的首地址。

delete的过程:1.先执行析构2.再释放空间

delete[]的过程:1.循环调用所有数组中对象的析构。2.再释放空间。

如果是new[]的空间,我们去用delete释放的话,可能会出现问题。

即,如果new[]的是内置类型,如int等,不需要调用构造和析构,那么用delete回收空间也是可以的。但是,如果new[]产生的是对象数组,仅仅调用delete去释放的话,一会造成内存泄露,因为它没有循环调用所有对象的析构,二会造成段错误,因为它并不是真正从首地址开始释放的,我们前面说了,new[]返回的地址空间前面还有四个字节呢。

A * p = new A[1];

//delete (A*)((int*)p-1);

delete p;

A是我们自定义的类,运行如上代码,直接调用delete程序会出错,但是我们将指针转换为int型指针,然后前移一个int空间,然后去释放,就没有出现问题。然后,我也尝试了往前移动其他字节大小,都出错了,所以基本上可以确认,new[]确实是在前面多给了四个字节。

但是如果把A换成内置类型,那么delete也不会出错了,我猜测,他并没有计算后面有多少个元素,也不需要计算。对象数组前面之所以要用四个字节保存长度,是因为它要循环调用析构,而内置类型并不需要。

 

如果new的空间,我们用delete[]释放。

我们可以推测,如果是内置类型的话,并不会出现任何问题。而如果是对象类型的话,由于delete[]释放时,会往前找四个字节看看有多少对象需要析构,而空间用new分配的,前面四个字节并不属于程序,所以必然会出错,经过试验验证,事实也是如此。

 

120.类对象赋值与初始化的区别

A a = A();(1)

A a;

a = A();(2)

第一条语句是初始化,它创建有指定值的对象,可能会创建临时对象(也可能不会)。第二条语句是赋值,像这样在赋值语句中使用构造函数总会导致在赋值前创建一个临时对象。

121.const成员函数

保证,函数不会修改调用对象。Const成员函数可与普通成员函数重载,const对象,只可以调用,const成员函数。而普通对象,既可以调用非const成员函数,又可以调用普通成员函数。当有const成员函数重载时,普通对象只能调用普通成员函数。

122.接受一个参数的构造

如果A的构造,接受一个int变量。那么可以使用

A a = 100;这种形式创建对象。

123.this指针的用处

a) 一个对象的this指针,并不是对象本身的一部分,不会影响sizeof(对象)的结果。、

b) this的作用域是在类的内部,当在类的非静态成员函数中访问类的非静态成员变量时,编译器会自动将对象本身的地址,作为一个隐含参数传递给函数。

c) 它作为非静态成员函数的隐形形参,对各成员的访问,均通过this指针进行。

123.this指针的生命周期

this指针在成员函数的开始前构造,在成员函数的结束后清除。和普通的函数参数一样。

124.类内定义常量的两种方式:

a) 定义枚举 enum{A,B};写在类内,只对类内有效。

b) 定义static const常量。static const int a = 125;可以直接写在类内。

125.静态成员的访问

a) 可以像访问普通成员一样,使用对象访问静态成员。

b) 可以不需要对象,直接类名加作用域运算符访问。

125.关于作用域内枚举

传统的枚举,可能存在两个枚举定义的枚举常量冲突。

比如enum a{A,B};enmu b{A,B};就会产生冲突。

所以,我们可以使用关键字class或者struct

enum class a{A,B};enum class b{A,B};

这样使用的时候,我们就可以用作用域区分开

a hehe = a::A;b haha = b::A;

就不会产生命名冲突的问题了。

126.将参数声明为引用的目的

提高效率,节省内存。、

 

127.重载的运算符,必须是有效的c++运算符,不能虚构一个新的运算符。

128.重载运算符的两种调用形式

a) 如果是在类内可以像调用成员函数一样,通过对象调用。例如加号运算符,A.operator+(B);如果是类外,就operator+(A,B);

b) 也可以像使用普通运算符一样,直接使用,隐式的调用重载。例如A+B

129.两种实现运算符重载的方式

a) 类内的成员函数重载运算符,但是如果是类内重载,第一个参数必须是调用对象。

b) 如果,第一个参数为非对象时,可以通过类外定义友元函数,实现运算符重载。

130.关于重载运算符的限制

a) 不能违背之前的运算规则,如单目运算符重载成双目运算符。

b) 同样不能修改运算符的优先级。

c) 不能创建新的没有的运算符。

d) 不能重载下面的运算符

i. Sizeof

ii. .成员运算符

iii. 成员指针运算符

iv. ::作用域运算符

v. ?:条件运算符

vi. 以及四种强转运算符

e) 下面的运算符只能通过成员函数重载

i. =赋值运算符

ii. ()函数调用运算符

iii. []下标运算符

iv. ->通过指针访问类成员的运算符

131.区分前++和后++的两种重载方式(类内or 类外)

class A

{

public:

int a;

public:

A(int a)

{

this->a = a;

}

 

A& operator++()//不带形参是前缀运算符

{

++a;

return *this;

}

 

A operator++(int x)//带形参是后缀运算符

{

int a = this->a;

++(this->a);

A b(a);

 

return b;

}

 

friend A& operator--(A&);//不带形参是前缀运算符

friend A operator--(A&,int);//带形参是后缀运算符

};

 

A& operator--(A&a)

{

--(a.a);

return a;

}

 

A operator--(A&a,int x)

{

int b = a.a;

--(a.a);

A c(b);

 

return c;

}

132.友元函数

a) 友元的机制是指允许类的非公有成员被一个类或者函数访问。

b) 友元按类型分为三种:

i. 普通非类成员函数作为友元

ii. 类的成员函数作为友元。

iii. 类作为友元

c) 友元的默认声明为extern,就是说友元类或者友元函数的作用域已经扩展到了包含该类定义的作用域,所以即便我们在类内部定义友元函数也是没有关系的。

133.类作为友元

类作为友元需要注意的是友元类和原始类之间的相互依赖关系,在友元类定义的文件中需要包含原始类定义的头文件。

friend class B;

134.类成员函数作为友元函数

因为你要类成员函数作为友元,你在声明友元的时候要用类限定符,所以必须先定义包含友元函数的类,但是在定义友元的函数的时候,又必须先定义原始类。通常的做法是先定义包含友元函数的类,再定义原始类,然后将原始类的声明,放在包含友元函数的类的前面。

 

 

135.友元函数的几条性质:

a) 友元不具有相互性,只具有单向性。即BA的友元,A不一定就是B的友元。

b) 友元不能被继承。BA的友元类,CB的子类,推不出CA的友元类。

c) 友元不具有传递性。BA的友元,CB的友元,推不出CA的友元。

136.C++static关键字

a) 作为静态全局变量,主要是声明该变量作用域限定在当前文件中。即静态全局变量不能为其他文件所用,其他文件可以定义相同名字的全局变量,而不用担心冲突。

b) 作为静态局部变量,有以下特点:

i. 该变量在全局数据区分配内存。

ii. 静态局部变量在程序执行到该对象的声明处事被首次初始化,即以后再调用都不会初始化。

iii. 静态局部变量一般在声明处初始化,如果没有显式初始化,会被程序自动初始化为0.

iv. 它始终驻留在全局数据区,所以它的生命周期直到程序运行结束。但是,它的作用域为局部作用域。

c) 静态函数

静态函数与普通函数的不同之处是它只能在声明它的文件当中可见,不能被其他文件使用。

定义静态函数的好处:

静态函数不能被其他文件所用

其他文件中可以定义相同名字的函数,不会发生冲突

d) 类内静态数据成员

i. 对所有的对象来说,静态数据成员只有一份,被他们所共享。而非静态数据成员,每个类对象都有自己的拷贝。

ii. 静态数据成员存储在全局数据区。静态数据成员定义时要分配内存,所以不能再类声明中定义。

iii. 静态数据成员也遵守,修饰符的限制(public,private,protected)。

iv. 静态数据成员,不依赖于任何对象,所以我们可以直接通过类名作用域访问它。

v. 静态数据成员即可以像访问普通数据成员一样通过对象访问,还可以直接类名作用域访问。

vi. 同全局变量相比,使用静态数据成员的优势:

1. 静态数据成员没有进入程序的全局名空间,所以不用担心命名冲突

2. 可以实现信息隐藏。静态数据成员可以是private成员,而全局变量不能。

 

e) 静态成员函数

i. 与普通成员函数相比,静态成员函数不与任何对象相联系,因此它不具有this指针。从这个意义上来讲,它无法访问属于类对象的非静态数据成员,也无法访问非静态成员函数,它只能调用其他的静态函数。

ii. 同理,静态成员函数也有两种调用方式。

 

137.默认的拷贝构造与赋值都是浅拷贝

138.类内的const成员常量,只能通过构造函数的初始化列表进行初始化

139.类内数据成员初始化的顺序与其在初始化列表中的顺序无关,只与其在类内声明的顺序有关。

140.继承的三种方式

a) 公有继承

i. 当类的继承方式为公有继承时,基类的公有和保护成员的访问属性在派生类中不变,而基类的私有成员不可访问。

b) 私有继承

i. 当类的继承方式为私有继承时,基类的公有成员和保护成员都以私有成员的身份出现在派生类中,而基类的私有成员在派生类中不可访问。

c) 保护继承

i. 当类的继承方式为保护继承时,基类的公有成员和保护成员都以保护成员的身份出现在派生类中。而基类的私有成员不可访问。

141.派生类

a) 派生类可以有多个基类,称为多继承,也可以只继承一个。

b) 如果不显示的给出继承方式,默认为private继承。继承方式指定派生类成员以及类外对象对于从基类继承来的成员的访问权限。

c) 派生类继承基类中除析构和构造函数以外的所有成员。

142.派生类的构造函数与析构函数

a) 派生类种由基类继承而来的成员的初始化工作还是由基类的构造完成,派生类派生的成员则在派生类的构造初始化。

b) 如果,基类中没有不带参数的构造函数,那么必须在派生类的构造函数中显示的调用基类构造函数,以初始化基类成员。

c) 基类构造执行顺序只与它们被继承的顺序相关

d) 类内对象的构造函数调用顺序,与它们在类内声明的顺序相关。

e) 析构时,先调用派生类的析构,再调用父类的析构。

143.如果某个派生类的多个基类拥有同名的成员,同时,派生类又新增这样的同名成员,在这种情况下,派生类成员将覆盖所有基类的同名成员。这时就需要类名作用域显示的调用父类的成员。

144.虚继承,虚基类

如果C有两个基类B1B2B1B2都继承自共同的基类A

在这种情况下,派生类C将会有A的两份成员的拷贝,在很多情况下我们只需要一份这样的数据拷贝,同一成员的多份拷贝将会增加内存开销。

为了解决这种多重拷贝的问题,可以将共同基类设置为虚基类,这时从不同路径继承过来的同名数据成员在内存就只有一份拷贝,同一个函数也只有一份映射。

语法:

Class 派生类名:virtual 继承方式 基类名

例:

Class A{};

Class B1:virtual public A{};

Class B2:virtual public A{};

Class C: public B1,public B2{};

 

145.虚基类及其派生类的构造函数

一般而言,派生类只对其直接基类的构造函数传递参数,但是在虚基类中,不管是直接或间接虚基类的所有派生类,都必须在构造函数的成员初始化列表中列出对虚基类的初始化。

例:

Class A

{

A(int a){}

};

 

Class B1:virtual public A

{

B1(int a):A(a){}

};

 

Class B2:virtual public A

{

B2(int a):A(a){}

};

 

Class C:public B1,public B2

{

C(int a):B1(a),B2(a),A(a){}

};

 

从以上例子中,看上去A的构造函数好像被调用三次,但事实上只有C类的构造函数真正调用了A类的构造。

146.赋值兼容原则

a) 派生类对象可以赋值给基类对象

b) 派生类对象可以初始化基类的引用

c) 派生类对象的地址可以赋值给指向基类的指针

d) 在赋值替代之后,派生类对象就可以作为基类的对象使用,但只能使用从基类继承的成员。

147.虚拟继承(内存模型)重点补充、、、

 

 

 

 

 

 

 

 

 

 

148.C++虚析构函数

a) 使用虚析构的作用,是为了当用一个基类的指针删除一个派生类的对象时,派生类的析构函数会被调用。

b) 当使用虚析构函数时,会为类添加一个虚函数表,会增加内存开销。所以,当类用作基类时,才考虑把析构函数定义为虚析构。

149.C++虚函数机制及多态的实现

所谓的多态,就是通过父类的引用或者指针去调用子类的实现。多态的机制是通过虚函数表来实现的。

每个定义了虚函数的类,都有一个指向该表的指针。并且总是在对象的最前边保存。

对于一般继承(即没有函数重写覆盖)

内存模型如下:

虚函数按照其声明顺序存储。父类的虚函数在子函数前面。

 

 

 

 

 

 

 

 

 

对于一般继承(有函数重写覆盖)

内存模型如下:

覆盖的函数放到父类中被覆盖函数原来的位置,没有覆盖的函数不变。

多重继承(无虚函数覆盖)

内存模型如下:

每个父类都有一个自己的虚函数表,并且子类的成员函数,被放到了第一个父类的表中(顺序是按照父类继承时声明的顺序)

 

 

多重继承(有虚函数覆盖)

内存模型如下:

不是覆盖父类的成员函数,仍然放在第一个虚函数表的原来的位置。而覆盖的虚函数,将覆盖所有表中被覆盖的父类的虚函数。

 

 

例如:有子类B。我们定义对象B b;

通过*(int*)(&b)就可以拿到对象b前四个字节的数据。而这个数据就是指向虚函数表的指针。

然后我们将这个指针,所指向的空间的前四个字节取出来,将是第一个虚函数的地址。

(pFun)*((int*)*(int*)(&b))//第一个函数地址

(pFun)*((int*)*(int*)(&b)+1)//第二个函数地址

150.C++四种类型强制转换

a) Const_cast,字面上理解就是去const属性

b) Static_cast,命名上理解是静态类型转换,如int转换为char

c) Dynamic_cast,命名上理解是动态类型转换。如子类和父类之间的多态类型转换。

d) Reinterpreter_cast,仅仅重新解释类型,但没有进行二进制转换。

151.const_cast作用详解

取去除const或者volatile属性。

152.类似于C风格的强制类型转换。无条件转换,静态类型转换,用于:

a) 基类和子类之间的转换:其中子类指针转换为父类是安全的;但父类指针转换为子类指针是不安全的。(推荐使用dynamic_cast

b) 基本数据类型之间的转换。Static_cast不能进行无关类型转换。如将int型指针转换为doube型。

c) 把空指针转换为目标类型的空指针

d) 把任何类型的表达式转换为void类型

e) Static_cast不能去除类型的const或者volatile类型

153.dynamic_cast转换,有条件的转换,动态类型转换,运行时类型安全检查(转换失败返回NULL

a) 安全的基类和子类之间的转换

b) 必须要有虚函数

c) 相同父类不同子类之间的交叉转换。但结果是NULL

154.reinterpreter_cast

a) 仅仅重新解释类型,但没有进行二进制转换。

b) 转换的类型必须是指针,引用,算数类型,函数指针或者成员指针。

c) 在比特位级别上进行转换。它可以把一个指针转换为一个整数,也可以把一个整数转换为一个指针。(但不能讲非32bit的实例转换为指针)

d) 最普通的用途就是在函数指针类型之间进行转换

e) 很难保证移植性

155.总结四种类型转换

i. 去const属性用const_cast

ii. 基本类型转换用static_cast(包括父子类型)

iii. 多态类型间的转换用dynamic_cast

iv. 不同类型的指针之间的转换用reinterpreter_cast

156.纯虚函数及抽象类

纯虚函数是在基类中只声明虚函数而不给出具体的函数定义体,将它的具体定义放在各派生类中,称此虚函数为纯虚函数.通过该基类的指针或引用就可以调用所有派生类的虚函数,基类只是用于继承,仅作为一个接口,具体功能在派生类中实现.

纯虚函数的声明如下:(注:要放在基类的定义体中)

   virtual 函数原型=0

声明了纯虚函数的类,称为抽象类。

抽象类中可以有多个纯虚函数

不能声明抽象类的对象,但可以声明指向抽象类的指针变量和引用变量

抽象类也可以定义其他非纯虚函数

如果派生类中没有重新定义基类中的纯虚函数,则在派生类中必须再将该虚函数声明为纯虚函数

从抽象类可以派生出具体或抽象类,但不能从具体类派生出抽象类

在一个复杂的类继承结构中,越上层的类抽象程度越高,有时甚至无法给出某些成员函数的实现,显然,抽象类是一种特殊的类,它一般处于类继承结构的较外层

引入抽象类的目的,主要是为了能将相关类组织在一个类继承结构中,并通过抽象类来为这些相关类提供统一的操作接口

 

157.一个C语言的小技巧

将一个,一个字节大小的数组,放在结构体的末尾,可以让这个结构体拥有可变大小的数组。

 

 

struct mumble

 {  

   char pc[ 1 ];  

};  

struct mumble *pmumb1 = ( struct mumble* )malloc(sizeof(struct mumble)+strlen(string)+1);  strcpy( &mumble.pc, string );

 

 

158.C++构造函数用作隐式转换函数

如果有构造函数A(double d);

我们定义A a; a = 19.3;

程序将会使用上述构造函数来创建一个临时对象,并将19.3作为初始值。随后采用逐个成员赋值方式将该临时对象的内容复制到a。这一过程称之为隐式转换。

将构造函数用做自动类型转换函数貌似不错,但是这种自动特性并不是总是符合需求的,因为这会导致意外的类型转换。因此C++增加关键字explicit用于关闭这种特性。

Explicit A(double d);

但是仍可以进行显示的强转。

A a; a = A(19.3);

 

159.编译器何时使用构造函数做转换

a) 如果使用了explicit,则将只能进行显示的强制转换

b) 隐式转换发生在下述情况

i. 将A对象初始化为double值时

ii. 将double值赋给A对象

iii. 将double传给接受A参数的函数时

iv. 返回值为A类型的函数,试图返回double值时

160.作为强转函数,参数匹配的过程

函数原型化提供的参数匹配过程,允许使用A(double)构造函数来转换其他数值类型。也就是说,下面两条语句都会先将int转换为double,然后使用A(double)构造函数。

A a(100);

a = 100;

然而,上述情况也只能用于转换不存在二义性时。也就是说如果函数还定义了A(long),那么编译器将会拒绝上述语句。

161.C++转换函数

将数字转换为对象可以,那么也可以做相反的转换。

这种相反的转换将不是使用构造函数。构造函数只完成从某种类型到类类型的转换。要进行相反的转换必须使用C++转换函数。

转换函数,是用户定义的强制类型转换,可以像使用强制类型转换那样转换他们。

意思就是,可以通过double(a)这种形式进行显示的强转,也可以通过

double d = a;让编译器进行隐式的强制转换,前提是有该种转换函数的定义。

 

 

 

 

162.如何创建转换函数

Operator typeName();

请注意一下几点:

(1)转换函数必须是类方法

(2)转换函数不能指定返回类型

(3)转换函数不能有参数

 

例如 operator double();

在这里double指出了要转换成的类型,因此不需要指定返回类型。转换函数是类方法意味着:它需要通过类对象来调用,从而告知函数要转换的值,因此,函数不需要参数。

 

例如 class A

{

Public:

Double m_d;

A(double d):m_d(d)

{

}

 

Operator double()

{

Return m_d;

}

Operator int()

{

Return int(m_d);

}

};

Int main()

{

A a(12.1);

Double d = a;

Cout<<int(a)<<endl;

}

 

解析:如果int(a)省略这种显示的强制类型转换,直接使用cout<<a的话,将会存在二义性。因为,编译器不知道是转换为double还是int,因此,如果只定义了一种转换函数的,就可以直接使用。

赋值的情况也是类似的

Long ll = a;因为int或者double都可以合法的转换为long型,因此也存在二义性。

所以,最好通过显式的强转,指定转换类型。

 

但是,它会隐式调用,有时候会产生一些问题。

例如:

Int ar[20];

A temp(12.3);

Int Temp = 1;

Cout<<ar[temp]<<endl;

原本想使用ar[Temp],不小心写成了ar[temp],但是由于隐式强转的存在,也不会有错误。

所以,原则上最好使用显式转换。使用explicit将转换函数声明为显式的。

 

 


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值