C++ 需要注意的一些问题

1. 常量必须在构造函数的初始化列表里面初始化。

class A { const int size = 0; }; 是错误的。 需要改成 class A{ A(){ const int size = 10; } };

或者改成 class A { static const int size = 10; };

2. 构造函数的初始化列表初始化变量的顺序是根据成员变量的声明顺序来执行的。

比如:

 1 class base
 2 
 3 {
 4  private:
 5 
 6   int m_i;
 7 
 8   int m_j;
 9 
10 public:
11 
12   base(int i): m_j(i), m_i(m_j) {}

就有错误,这里的m_i将会被初始化成一个随机值。

3. 基类的析构函数必须是虚函数。因为这样处理后,所有子类的析构函数都将会被自动变为virtual类型,这就保证了在任何情况下,不会出现由于析构函数未被调用而导致的内存泄露。

 

4. 为什么不能将构造函数定义为虚函数?

虚拟函数是采用一种虚调用的办法。虚调用是一种可以在只有部分信息的情况下工作的机制,特别允许我们调用一个只知道接口而不知道其准确对象类型的函数。但是如果创建一个对象,你应该知道对象的准确对象,因此构造函数不能为虚函数。

 

5. 为什么不能把每个函数都声明为虚函数?

虚函数是由代价的:由于每个虚函数都维护一个v表,因此在使用虚函数的时候会产生一个系统开销。如果仅仅是一个很小的类,且不想派生其他类,那么根本就没有必要使用虚函数。

6. 析构函数可以是内联函数吗?

答案是可以的。

7. 单参数的构造函数如果不添加explicit关键字,会定义一个隐含的类型转换,添加explicit关键字会消除这种类型转换。

8. 如果类中包含指针的话,一般需要编写拷贝构造函数和拷贝赋值运算符,以避免浅拷贝。

9. 为什么需要友元?

类具有封装和信息隐藏的特性。只有类的成员函数才能访问类的私有成员,程序中的其他函数是无法访问私有成员的。非成员函数可以访问

类的公有成员,但是如果将数据成员都定义成公有的,这又破坏了隐藏的特性。另外,应该看到在某些情况下,特别是在对某些成员函数多次调

用时,由于参数传递,类型检查和安全性检查都需要时间开销,而影响程序的运行效率。

为了解决这个问题,提出了一种使用友元的方案。友元是一种定义在类外部的普通函数。但它需要在类体内进行说明,为了与该类的成员函数加

以区别,在说明时前面加以关键字friend。友元不是成员函数,但是可以访问类的私有成员。作用在于提高程序的运行效率,但是,它破坏了类

的封装性和隐藏性,使得非成员函数可以访问类的私有成员。

10. C++中如何阻止一个类被实例化?

使用抽象类,或者构造函数被声明为private

11. 什么时候编译器会生成copy constructor呢?

只要自己没有编写,程序中需要,都会生成

12. C++引入的开销体现在以下两个方面:

1.编译时的开销

模板,类层次结构,强类型检查等新特性,以及大量使用了这些新特性的C++模板,算法库都明显地增加了C++编译器的负担。但是应当看到,这些新技能在不增加程序执行效率的前提下,明显降低了广大C++程序员的工作量。

2.运行时的开销

相对于传统的C程序而言,C++中可能引入额外运行时的开销特性包括:

虚基类,虚函数,RTTI(dynamic_cast, typeid), 异常以及对象的构造和析构

虚基类,从直接虚继承的子类中访问虚基类的数据成员或其虚函数时,将增加两次指针引用(大部分情况下可以优化为一次)和一次整型加法的时间开销。定义一个虚基类表,定义若干虚基类表指针的空间开销。

虚函数的运行开销有进行整形加法和指针引用的时间开销。定义一个虚表,定义若干个虚表指针的空间开销。

RTTI的运行开销主要有进行整形比较和取址操作(可能还会有一两次整形加法)所增加的时间开销。定义一个type_info对象(包括类型ID和类名称)的空间开销。"dynamic_cast"用于在类层次结构中漫游,对指针或者引用进行自由地向上,向下或者交叉转化。"typeid"则用于获取一个对象或者引用的确切类型。一般地讲,能用虚函数解决的问题就不要用"dynamic_cast",能够用"dynamic_cast"解决的就不用"typeid".

关于异常,对于几乎所有的编译器来说,在正常情况下,try块中的代码执行效率和普通代码一样高,而且由于不需要使用传统的通过返回值函数调用来判断错误的方式,代码的执行效率还会进一步提高。抛出和捕捉异常的开销也只是在某些情况下会高于函数返回和函数调用的开销。

关于构造和析构,开销也不总是存在的。对于不需要初始化/销毁的类型,并没有构造和析构的开销,相反对于那些需要初始化/销毁的类型来说,即使使用传统的C方式来实现,也至少需要与之相当的开销。

当然,RTTI是有用的。但因为一些理论上以及方法论上的原因,它破坏了面向对象的纯洁性。

首先,它破坏了抽象,使得一些本来不应该被使用的方法和属性被不正确地使用。其次,因为运行时类型的不确定性,它把程序变得更脆弱。第三点,也是最重要的一点,它使得程序缺乏扩展性。当加入一个新的特性时候,你也许需要仔细阅读你的dynamic_cast或者instanceof的代码,必要时改动它们,以保证这个新的类型加入不会导致问题。而这个过程中,编译器将不会给你任何帮助。

13. 请描述heap与stack的区别

在进行c/c++编程时,需要程序员对内存的了解比较精准。经常需要操作的内存可以分为:

栈区:由编译器自动分配和释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈

堆区:一般由程序员分配和释放,若程序员不释放,程序结束时可能由操作系统回收。注意它与数据结构中的堆是两回事,分配方式倒是类似链表。

全局区(静态区): 全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未释放的静态变量在相邻的另一块区域。程序结束后由系统释放。

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

程序代码区:存放函数体的二进制代码。

 1 int a = 0; // 全局初始化区
 2 char *p1; // 全局未初始化
 3 main()
 4 {
 5     int b; //
 6     char s[] = "abc"; //
 7     char *p2;  //
 8     char *p3 = "123456"; // "123456"在常量区,p3在栈上
 9     static int c = 0; // 全局(静态)初始化区
10     p1 = (char *)malloc(10);
11     p2 = (char *)malloc(20); 
12     // 分配得来的10和20字节的区域就在堆区
13     strcpy(p1, "123456");
14     //"123456"放在常量区,编译器可能会将它与p3所指向的"123456"优化成一个地方   
15 }

堆和栈的理论知识:

1. 申请方式

栈:由系统自动分配。比如,声明在函数中的一个局部变量int b,系统自动在栈中为b开辟空间。

堆:需要程序员自己申请,并指明大小,在C中使用malloc函数。

比如 p1 = (char *)malloc(10);

在C++中使用new运算符 ,比如:

p2 = new char;

但是,注意p1,p2自身在栈中的。

2. 申请后的系统的响应

栈:只要栈的剩余空间大于所申请的空间,系统将为程序提供内存,否则将报以长提示栈溢出。

堆:首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历链表,寻找第一个空间大于所申请空间的堆节

点,然后将该节点从空闲节点链表中删除,并将该节点的空间分配给程序。对于大多数系统,会在这块内存空间中的首地址处记录本次分配的

大小,这样,代码的delete语句才能正确地释放本内存空间。另外,由于找到的堆节点的大小不一定正好等于申请的大小,系统会自动将多余的

那部分重新加入空余链表中。

3. 申请大小的限制

栈:在windows下,栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,在windows下,栈的大小是2MB,也有的说是1MB,总之是一个编译时候就确定的常数。如果申请的空间超过栈的剩余空间,将提示overflow。因此,能从栈获得的空间较小。

堆:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是链表存储空闲内存地址的,自然是不连续的。而链表的遍历方向是由低地址向高地址,堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。

申请效率的比较:

栈:由系统自动分配,速度较快。但程序员无法控制。

堆:使用malloc/new分配的,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便。另外,在windows下,最好的方式是用virtualAlloc分配内存。不是在堆,也不是在栈,而是直接在进程的地址空间中保留一块内存,虽然用起来最不方便,但是速度最快,也最灵活。

堆和栈中的存储内容

栈:在函数调用时候,第一个进栈的是主函数中的下一条指令(函数调用语句的下一条可执行语句)的地址,然后是函数的各个参数。在大多数的

C编译器中,参数是由右往左入栈的,然后是函数的局部变量。注意静态变量是不入栈的。

当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是主函数中的下一条指令,程序由该点继续运

行。

堆:一般是在堆得头部用一个字节表示存放堆的大小。堆中的具体内容由程序员安排。

存取效率的比较

char s1[] = "aaaaaa";

char *s2 = "bbbbb";

"aaaaaa"是在运行时刻赋值的,而"bbbbb"是在编译时刻就确定的。但是,在以后的存取中,在栈上的数组比指针所指向的字符串快。

 

posted @ 2015-03-24 15:02  David_W  阅读(432)  评论(0编辑  收藏  举报