面试笔试---C\C++查漏补缺

来源:互联网 发布:施耐德base 知乎 编辑:程序博客网 时间:2024/04/30 10:21

记录下C++方面自己掌握还不够得地方,有在网上看到的,或在题目中碰到不会的,在此记录下。


1.C和C++中的static有什么作用

1.隐藏。(static函数,static变量均可)

当同时编译多个文件时,所有未加static前缀的全局变量和函数都具有全局可见性。
举例来说明。同时编译两个源文件,一个是a.c,另一个是main.c。

//a.cchar a = 'A';               // global variablevoid msg(){     printf("Hello\n");}//main.cint main(){     extern char a;       // extern variable must be declared before use     printf("%c ", a);     (void)msg();     return 0;}

程序的运行结果是:

A Hello

 

 

为什么在a.c中定义的全局变量a和函数msg能在main.c中使用?前面说过,所有未加static前缀的全局变量和函数都具有全局可见性,其它的源文件也能访问。此例中,a是全局变量,msg是函数,并且都没有加static前缀,因此对于另外的源文件main.c是可见的。
如果加了static,就会对其它源文件隐藏。例如在a和msg的定义前加上static,main.c就看不到它们了。利用这一特性可以在不同的文件中定义同名函数和同名变量,而不必担心命名冲突。static可以用作函数和变量的前缀,对于函数来讲,static的作用仅限于隐藏.

2.static的第二个作用是保持变量内容的持久。(static变量中的记忆功能和全局生存期)

存储在静态数据区的变量会在程序刚开始运行时就完成初始化,也是唯一的一次初始化。共有两种变量存储在静态存储区:全局变量和static变量,只不过和全局变量比起来,static可以控制变量的可见范围,说到底static还是用来隐藏的。虽然这种用法不常见

PS:如果作为static局部变量在函数内定义,它的生存期为整个源程序,但是其作用域仍与自动变量相同,只能在定义该变量的函数内使用该变量。退出该函数后, 尽管该变量还继续存在,但不能使用它。

基于以上两点可以得出一个结论:把局部变量改变为静态变量后是改变了它的存储方式即改变了它的生存期。把全局变量改变为静态变量后是改变了它的作用域, 限制了它的使用范围。因此static 这个说明符在不同的地方所起的作用是不同的。

3.static的第三个作用是默认初始化为0(static变量)

其实全局变量也具备这一属性,因为全局变量也存储在静态数据区。在静态数据区,内存中所有的字节默认值都是0x00,某些时候这一特点可以减少程序员的工作量。比如初始化一个稀疏矩阵,我们可以一个一个地把所有元素都置0,然后把不是0的几个元素赋值。如果定义成静态的,就省去了一开始置0的操作。再比如要把一个字符数组当字符串来用,但又觉得每次在字符数组末尾加‘\0’;太麻烦。如果把字符串定义成静态的,就省去了这个麻烦,因为那里本来就是‘\0’。

最后对static的三条作用做一句话总结。首先static的最主要功能是隐藏,其次因为static变量存放在静态存储区,所以它具备持久性和默认值0.


4.static的第四个作用:C++中的类成员声明static(有些地方与以上作用重叠)

 在类中声明static变量或者函数时,初始化时使用作用域运算符来标明它所属类,因此,静态数据成员是类的成员,而不是对象的成员,这样就出现以下作用:

(1)类的静态成员函数属于整个类而非类的对象,所以它没有this指针,这就导致 了它仅能访问类的静态数据和静态成员函数。      

(2)不能将静态成员函数定义为虚函数。      

(3)由于静态成员声明于类中,操作于其外,所以对其取地址操作,就多少有些特殊 ,变量地址是指向其数据类型的指针 ,函数地址类型是一个“nonmember函数指针”。

(4)由于静态成员函数没有this指针,所以就差不多等同于nonmember函数,结果就 产生了一个意想不到的好处:成为一个callback函数,使得我们得以将C++和C-based X W indow系统结合,同时也成功的应用于线程函数身上。 (这条没遇见过)  

(5)static并没有增加程序的时空开销,相反她还缩短了子类对父类静态成员的访问 时间,节省了子类的内存空间。      

(6)静态数据成员在<定义或说明>时前面加关键字static。      

(7)静态数据成员是静态存储的,所以必须对它进行初始化 (程序员手动初始化,否则编译时一般不会报错,但是在Link时会报错误)     

(8)静态成员初始化与一般数据成员初始化不同:

初始化在类体外进行,而前面不加static,以免与一般静态变量或对象相混淆;
初始化时不加该成员的访问权限控制符private,public等;        
初始化时使用作用域运算符来标明它所属类;
           所以我们得出静态数据成员初始化的格式:
<数据类型><类名>::<静态数据成员名>=<值>

(9)为了防止父类的影响,可以在子类定义一个与父类相同的静态变量,以屏蔽父类的影响。这里有一点需要注意:我们说静态成员为父类和子类共享,但我们有重复定义了静态成员,这会不会引起错误呢?不会,我们的编译器采用了一种绝妙的手法:name-mangling 用以生成唯一的标志

 引申2:在头文件中定义静态变量,是否可行?为什么?
        不可行,如果在头文件中定义静态变量,会造成资源浪费的问题,同时也可能引起程序的错误。因为如果在使用了该头文件的每个 C 语言文件中定义静态变量,在每个头文件中都会单独存在一个静态变量,从而会引起空间浪费或者程序出错。

类没有实例化前,只能用静态成员函数操作


 引申1:在头文件中定义静态变量,是否可行?为什么?
        不可行,如果在头文件中定义静态变量,会造成资源浪费的问题,同时也可能引起程序的错误。因为如果在使用了该头文件的每个 C 语言文件中定义静态变量,在每个头文件中都会单独存在一个静态变量,从而会引起空间浪费或者程序出错。


引申2:全局变量和静态变量的存储方式是一样的,只是作用域不同。如果它们未初始化或初始化为0则会存储在BSS段,如果初始化为非0值则会存储在DATA段,

引申3:静态变量和全局变量的区别在于非静态全局变量的作用域是整个源程序, 当一个源程序由多个源文件组成时,非静态的全局变量在各个源文件中都是有效的。 而静态全局变量则限制了其作用域, 即只在定义该变量的源文件内有效, 在同一源程序的其它源文件中不能使用它。由于静态全局变量的作用域局限于一个源文件内,只能为该源文件内的函数公用, 因此可以避免在其它源文件中引起错误。

2.C\C++内存分配方式有哪几种?


1)从静态存储区域分配。程序中定义的全局变量和static变量就是这种方式分配内存的。内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。 

BSS段:(bss segment)通常是指用来存放程序中未初始化全局变量具体体现为一个占位符的一块内存区域。BSS是英文Block Started by Symbol的简称。BSS段属于静态内存分配。

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

数据段 :数据段(data segment)通常是指用来存放程序中 已初始化  全局变量 的一块内存区域。数据段属于静态内存分配。.bss是不占用.exe文件空间的,其内容由操作系统初始化(清零);而.data却需要占用,其内容由程序初始化,因此造成了上述情况。 

代码段 代码段(code segment/text segment)通常是指用来存放 程序执行代码 的一块内存区域。这部分区域的大小在程序运行前就已经确定,并且内存区域通常属于 只读 , 某些架构也允许代码段为可写,即允许修改程序。在代码段中,也有可能包含一些 只读的常数变量 ,例如字符串常量等。程序段为程序代码在内存中的映射.一个程序可以在内存中多有个副本.

2)在栈上创建程序中的局部变量就是这种情况的内存分配方式。在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。除此以外,在函数被调用时,栈用来传递参数和返回值。由于栈的先进先出特点,所以栈特别方便用来保存/恢复调用现场栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。 

3)从堆上分配,亦称动态内存分配。程序在运行的时候用malloc或new申请任意多少的内存,程序员自己负责在何时用free或delete 释放内存。

#include<iostream.h>void main(){char a[]="abc";栈 char b[]="abc";栈 char* c="abc";abc在常量区,c在栈上。char* d="abc"; 编译器可能会将它与c所指向的"abc"优化成一个地方。const char e[]="abc";栈 const char f[]="abc";栈 cout << a << " " << b << " " << c << " " <<d << " " <<e << " " <<f <<endl;cout<<(a==b?1:0)<<endl<<(c==d?1:0)<<endl<<(e==f?1:0)<<endl;}
以上程序的输出结果为 
abc abc abc abc abc abc 
0
1
0

3.malloc和new的区别?

详见


4.extern作用

(1).与“C”连用

extern "C" void fun(int a, int b);则告诉编译器在编译fun这个函数名时按着C的规则去翻译相应的函数名而不是C++的。

在此引申出另一个问题:如果C++程序要调用已经被编译后的C函数,会发生什么?

 C++程序不能直接调用已编译后的C函数的,这是因为名称问题,比如,函数void foo(int x, int y),该函数被C编译器编译后在库中的名字为_foo,而C++编译器则会产生像_foo_int_int之类的名字用来支持函数重载和类型安全连接,名称就不一样,因此不能直接调用的。

(2).不与“C”连用

当extern不与"C"在一起修饰变量或函数时,如在头文件中: extern int g_Int; 它的作用就是声明函数或全局变量的作用范围的关键字,其声明的函数和变量可以在本模块或其他模块中使用,记住它是一个声明不是定义!也就是说B模块(编译单元)要是引用模块(编译单元)A中定义的全局变量或函数时,它只要包含A模块的头文件即可,在编译阶段,模块B虽然找不到该函数或变量,但它不会报错,它会在连接时从模块A生成的目标代码中找到此函数。

5.重载,覆盖和隐藏

http://blog.csdn.net/haoel/article/details/1948051

成员函数被重载的特征:

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

            (2)函数名字相同;

            (3)参数不同;

            (4)virtual关键字可有可无。

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

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

            (2)函数名字相同;

            (3)参数相同;

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

隐藏特征如下:

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

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

<span style="font-size:14px;">#include <iostream.h> class Base { public: virtual void f(float x){ cout << "Base::f(float) " << x << endl; } void g(float x){ cout << "Base::g(float) " << x << endl; } void h(float x){ cout << "Base::h(float) " << x << endl; } }; class Derived : public Base { public: virtual void f(float x){ cout << "Derived::f(float) " << x << endl; } void g(int x){ cout << "Derived::g(int) " << x << endl; } void h(float x){ cout << "Derived::h(float) " << x << endl; } </span>

(1)函数Derived::f(float)覆盖了Base::f(float)。

(2)函数Derived::g(int)隐藏了Base::g(float),而不是重载。

(3)函数Derived::h(float)隐藏了Base::h(float),而不是覆盖。


6.函数调用过程

点击打开链接

7.用递归和非递归两种方法翻转一个链表

<span style="font-size:14px;">typedef struct node{ElemType data;struct node * next;}ListNode;typedef struct{ListNode *head;int size;ListNode *tail;}List;/*********************************************************非递归的翻转实际上就是使用循环,依次后移指针,并将遇到的链表指针反转*********************************************************/void ReserveList(List * plist)        //非递归实现,{ListNode * phead;//新链表的头 开始的第一个节点ListNode * pt; //旧链表的头 开始的第二个节点ListNode * pn; //旧链表头的下一个phead = plist->head;if(phead && phead->next&& phead->next->next) //首先确定{phead = plist->head->next;//新链表就是以第一个节点开始,依次在表头添加节点,添加的节点是旧链表的第一个节点pt = phead->next; //旧链表,旧链表被取走头结点之后放入新链表的表头,pn = pt->next;phead->next = 0;while(pt){pn = pt->next; //pn是旧链表的第二个节点pt ->next = phead;//取旧链表的第一个节点插入新链表phead = pt;pt = pn; //旧链表往后移动}}plist->head->next = phead; //新链表重新赋值到整个链表}/*********************************************************递归思想,原理也是从就链表上依次取元素放入到新链表直到原始链表被取完,得到新链表*********************************************************/ListNode * ReserveListRe(ListNode * oldlist,ListNode * newlist){ListNode * pt;pt = oldlist->next; //取旧链表的表头,pt是现在的旧链表oldlist->next = newlist;//就旧链表插入到新链表newlist = oldlist; //如果旧链表是空,表示旧链表被取完了,新链表就是翻转之后的链表return (pt == NULL) ? newlist : ReserveListRe(pt,newlist);} </span>
8.delete 与 delete []有什么区别

C++告诉我们在回收用 new 分配的单个对象的内存空间的时候用 delete,回收用 new[] 分配的一组对象的内存空间的时候用 delete[]。 
关于 new[] 和 delete[],其中又分为两种情况:(1) 为基本数据类型分配和回收空间;(2) 为自定义类型分配和回收空间。

<span style="font-size:14px;">#include <iostream>;using namespace std; class T {public:  T() { cout << "constructor" << endl; }  ~T() { cout << "destructor" << endl; }}; int main(){  const int NUM = 3;   T* p1 = new T[NUM];  cout << hex << p1 << endl;  //  delete[] p1;  delete p1;   T* p2 = new T[NUM];  cout << p2 << endl;  delete[] p2;}</span>
从运行结果中我们可以看出,delete p1 在回收空间的过程中,只有 p1[0] 这个对象调用了析构函数,其它对象如 p1[1]、p1[2] 等都没有调用自身的析构函数,这就是问题的症结所在。如果用 delete[],则在回收空间之前所有对象都会首先调用自己的析构函数。 
    基本类型的对象没有析构函数,所以回收基本类型组成的数组空间用 delete 和 delete[] 都是应该可以的;但是对于类对象数组,只能用 delete[]。对于 new 的单个对象,只能用 delete 不能用 delete[] 回收空间。 
    所以一个简单的使用原则就是:new 和 delete、new[] 和 delete[] 对应使用。

9.拷贝构造函数在哪几种情况下会被调用?

      1).当类的一个对象去初始化该类的另一个对象时

      2).如果函数的形参是类的对象,调用函数进行形参和实参结合时

      3).如果函数的返回值是类对象,函数调用完成返回时

对象在创建时使用其他的对象初始化

Person p(q); //此时复制构造函数被用来创建实例p

Person p = q; //此时复制构造函数被用来在定义实例p时初始化p

对象作为函数的参数进行值传递时

f(p); //此时p作为函数的参数进行值传递,p入栈时会调用复制构造函数创建一个局部对象,与函数内的局部变量具有相同的作用域

需要注意的是,赋值并不会调用复制构造函数,赋值只是赋值运算符(重载)在起作用

p = q; //此时没有复制构造函数的调用!

简单来记的话就是,如果对象在声明的同时将另一个已存在的对象赋给它,就会调用复制构造函数;如果对象已经存在,然后将另一个已存在的对象赋给它,调用的就是赋值运算符(重载)

默认的复制构造函数和赋值运算符进行的都是"shallow copy",只是简单地复制字段,因此如果对象中含有动态分配的内存,就需要我们自己重写复制构造函数或者重载赋值运算符来实现"deep copy",确保数据的完整性和安全性。


10.inline函数为什么不能为虚函数

内联函数不能为虚函数,原因在于虚表机制需要一个真正的函数地址,而内联函数展开以后,就不是一个函数,而是一段简单的代码(多数C++对象模型使用虚表实现多态,对此标准提供支持),可能有些内联函数会无法内联展开,而编译成为函数。

11.C++异常处理

(1).栈展开

当抛出一个异常之后,程序暂停当前函数的执行过程,并立即开始寻找与异常匹配的catch子句。如果对抛出异常的函数的调用语句位于一个try语句块内,则检查与该try块关联的catch子句。如果找到了匹配的catch,就使用该catch处理异常。否则,如果该try语句嵌套在其他try块中,则继续检查与外层try匹配的catch子句。如果仍然没有找到匹配的catch,则退出当前这个主调函数,继续在调用了刚刚退出的这个函数的其他函数中寻找,以此类推。

这个过程称为栈展开。栈展开过程沿着嵌套函数的调用链不断查找,直到找到了与异常匹配的catch语句为止;或者也可能一直没有找到匹配的catch,则退出主函数后查找过程终止。

一个异常如果没有被捕获,则它将终止当前的程序(调用terminate)。

栈展开过程对象(局部)被自动销毁(类类型自动调用析构函数)。出于栈展开使用析构函数的考虑,析构函数不应该抛出不能被自身处理的异常。


(2)catch(...)

捕获所有异常。既能单独出现,也能与其他几个catch语句一起出现(一起出现,必须在最后的位置)。


(3)函数try语句块和构造函数

构造函数体内的catch语句无法处理构造函数初始列表抛出的异常,解决方法,将构造函数写成函数try语句块。

<span style="font-size:14px;">template<typename T>Blob<T>::Blob(std::initializer_list<T> il)try:data(std:make_sharde<std::vector<T> >(il)){/*函数体*/}catch(const std::bad_alloc &e){handle_out_of_memory(e);}</span>

既能处理构造函数体抛出的异常,也能处理成员函数初始化列表抛出的异常。


(4)noexcept

C++11中,通过noexcept说明指定某个函数不会抛出异常。


void recoup(int)  nocept;

两种情况下使用:

a.确认函数不会抛出异常

b.不知道该如何处理异常


noexcept说明符包含一个bool类型可选实参:


void recoup(int)  nocept(true);//不会抛出

void recoup(int)  nocept(false);//可能抛出


noexcept说明符的实参常常与noexcept运算符混合使用。noexcept运算符是一个一元运算符,返回值是一个bool类型的右值常量表达式,用于表示给定的表达式是否会抛出异常。

nocept(recoup(i))i;//如果recopu不抛出异常,结果为true;反之false



(5)异常类层次


(6)一个实例学习


(7)对c++两种处理方式,throw抛出异常和return错误码的理解。


     return错误码比较常用,但是有时不合适,例如返回错误码是int,每个调用都要检查错误值,极不方便,也容易让程序规模加倍(但是要精确控制逻辑,这种方法不错)。return error 调用者一偷懒就忽略了。返回 Error 除非当场处理,需要层层传递,或者用全局变量记录,不然就会被忽略。一个层次忽略了,就失去机会处理了。

      而对于异常抛出,就把错误和处理分开来,由库函数抛出异常,由调用者捕获这个异常,调用者就可以知道程序函数库调用出现错误了,并去处理,而是否终止程序就把握在调用者手里了。exception 忽略掉程序直接就挂了。有了异常,分工更明确了。内层代码,不必费心解决异常逻辑,抛出异常即可;内层异常,本层次也不是必须处处捕捉异常的那一个层次,必须处理异常,再捕捉处理。永远有机会处理之。


1.错误判断和描述,可以交给系统和编译器来完成,自己不需要写大量的if;

2.随时随地都可以跳出当前的执行过程,且不管有多少层(用return,你每层都要判断,都要return)

总的来说,异常抛出,能使得程序更加简洁,更加可靠。

(12)嵌套依赖类型名

C++ 有一条规则:如果解析器在一个 template(模板)中遇到一个 nested dependent name(嵌套依赖名字),它假定那个名字不是一个 type(类型),除非你用其它方式告诉它。缺省情况下,nested dependent name(嵌套依赖名字)不是 types(类型)。

<span style="font-size:14px;">template<typename C>void print2nd(const C& container){ if (container.size() >= 2) {  C::const_iterator iter(container.begin()); // this name is assumed to  ... // not be a type</span>
这为什么不是合法的 C++ 现在应该很清楚了。iter 的 declaration(声明)仅仅在 C::const_iterator 是一个 type(类型)时才有意义,但是我们没有告诉 C++ 它是,而 C++ 就假定它不是。要想转变这个形势,我们必须告诉 C++ C::const_iterator 是一个 type(类型)。我们将 typename 放在紧挨着它的前面来做到这一点:

<span style="font-size:14px;">template<typename Comparable>typename AvlTree<Comparable>::AvlNode * AvlTree<Comparable>::findMax(AvlNode *t) const {if (t == NULL)return NULL;if (t->right == NULL)return t;return findMax(t->left);}</span>

或:

<span style="font-size:14px;">template<typename C> // this is valid C++void print2nd(const C& container){if (container.size() >= 2) {typename C::const_iterator iter(container.begin());...}}</span>
通用的规则很简单:在你涉及到一个在 template(模板)中的 nested dependent type name(嵌套依赖类型名)的任何时候,你必须把单词 typename 放在紧挨着它的前面。
规则的例外是 typename 不必前置于在一个 list of base classes(基类列表)中的或者在一个 member initialization list(成员初始化列表)中作为一个 base classes identifier(基类标识符)的 nested dependent type name(嵌套依赖类型名)。例如:
<span style="font-size:14px;">template<typename T>class Derived: public Base<T>::Nested {  // base class list: typename not public: // allowed  explicit Derived(int x)  : Base<T>::Nested(x) // base class identifier in mem  {    // init. list: typename not allowed    typename Base<T>::Nested temp; // use of nested dependent type   ... // name not in a base class list or  } // as a base class identifier in a  ... // mem. init. list: typename required};</span>
详细
(13)堆空间和栈空间

 栈是系统提供的功能,特点是快速高效,缺点是有限制,数据不灵活;而堆是函数库提供的功能,特点是灵活方便,数据适应面广泛,但是效率有一定降低。栈是系统数据结构,对于进程 线程是唯一的;堆是函数库内部数据结构,不一定唯一。不同堆分配的内存无法互相操作。栈的动态分配无需释放(是自动的),也就没有释放函数。为可移植的程序起见,栈的动态分配操作是不被鼓励的!堆空间的分配总是动态的,虽然程序结束时所有的数据空间都会被释放回系统,但是精确的申请内存 释放内存匹配是良好程序的基本要素。

主要的区别由以下几点:      

、管理方式不同;      

2、空间大小不同;      

、能否产生碎片不同;      

、生长方向不同;      

、分配方式不同;      

、分配效率不同;

管理方式:对于栈来讲,是由编译器自动管理,无需我们手工控制;对于堆来说,释放工作由程序员控制,容易产生 memory leak 。 

空间大小:一般来讲在 32 位系统下,堆内存可以达到 4G 的空间,从这个角度来看堆内存几乎是没有什么限制的。但是对于栈来讲,一般都是有一定的空间大小的.

碎片问题:对于堆来讲,频繁的 new/delete 势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。对于栈来讲,则不会存在这个问题,因为栈是先进后出的队列,他们是如此地一一对应,以至于永远都不可能有一个内存块从栈中间弹出,在他弹出之前,在他上面的后进的栈内容已经被弹出,详细的可以参考数据结构,这里我们就不再一一讨论了。 

生长方向:对于堆来讲,生长方向是向上的,也就是向着内存地址增加的方向;对于栈来讲,它的生长方向是向下的,是向着内存地址减小的方向增长。 

分配方式:堆都是动态分配的,没有静态分配的堆。栈有 种分配方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配。动态分配由 alloca 函数进行分配,但是栈的动态分配和堆是不同的,栈的动态分配是由编译器进行释放,无需我们手工实现。 

分配效率:栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。堆则是 C/C++函数库提供的,它的机制是很复杂的,例如为了分配一块内存,库函数会按照一定的算法(具体的算法可以参考数据结构 操作系统)在堆内存中搜索可用的足够大小的空间,如果没有足够大小的空间(可能是由于内存碎片太多),就有可能调用系统功能去增加程序数据段的内存空间,这样就有机会分到足够大小的内存,然后进行返回。显然,堆的效率比栈要低得多。 

(14)C++内存

一个由C/C++编译的程序占用的内存分为以下几个部分:

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

 2、堆区(heap):一般是由程序员分配释放,若程序员不释放的话,程序结束时可能由OS回收,值得注意的是他与数据结构的堆是两回事,分配方式倒是类似于数据结构的链表。

 3、全局区(static):也叫静态数据内存空间,存储全局变量和静态变量,全局变量和静态变量的存储是放一块的,初始化的全局变量和静态变量放一块区域,没有初始化的在相邻的另一块区域,程序结束后由系统释放。

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

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

 

堆和栈的区别:

1、由以上综述就可以得知,他们程序的内存分配方式不同。

2、申请和响应不同:

(1)申请方式:

stack由系统自动分配,系统收回;heap需要程序员自己申请,C中用函数malloc分配空间,用free释放,C++用new分配,用delete释放。

(2)申请后系统的响应:

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

堆:首先应该知道操作系统有一个记录内存地址的链表,当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请的空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序。另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样代码中的delete或free语句就能够正确的释放本内存空间。另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会将多余的那部分重新放入空闲链表中。

3、申请的大小限制不同:

栈:在windows下,栈是向低地址扩展的数据结构,是一块连续的内存区域,栈顶的地址和栈的最大容量是系统预先规定好的,能从栈获得的空间较小。

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

 4、申请的效率不同:

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

堆:堆是有程序员自己分配,速度较慢,容易产生碎片,不过用起来方便。

5、堆和栈的存储内容不同:

栈:在函数调用时,第一个进栈的是主函数中函数调用后的下一条指令的地址,然后是函数的各个参数,在大多数的C编译器中,参数是从右往左入栈的,当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是主函数中的下一条指令。

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


(15)float类型与0比较

const float EPSINON = 0.00001; 

if ((x >= - EPSINON) && (x <= EPSINON)

不可将浮点变量用“==”或“!= ”与数字比较,应该设法转化成“>=”或“ <=”此类形式


(16)const成员函数

1)const成员函数可以访问非const对象的非const数据成员、const数据成员,也可以访问const对象内的所有数据成员;

2)非const成员函数可以访问非const对象的非const数据成员、const数据成员,但不可以访问const对象的任意数据成员;

3)作为一种良好的编程风格,在声明一个成员函数时,若该成员函数并不对数据成员进行修改操作,应尽可能将该成员函数声明为const 成员函数。

为了确保const对象的数据成员不会被改变,在C++中,const对象只能调用const成员函数


a.常成员函数不能更新对象的数据成员

b.当一个对象被声明为常对象,则不能通过该对象调用该类中的非const成员函数


(17)static成员变量和成员函数


1)静态成员函数不与任何对象绑定在一起,它们不包含this指针。作为结果,静态成员函数不能声明成const的,也不能在static函数体内使用this指针。

2)在C++中,类的静态成员(static member)必须在类内声明,在类外初始化

<span style="font-size:14px;">class A{  private:    static int count ; // 类内声明};int A::count = 0 ; // 类外初始化,不必再加static关键字</span>
为什么?因为静态成员属于整个类,而不属于某个对象,如果在类内初始化,会导致每个对象都包含该静态成员,这是矛盾的。
被static声明的类静态数据成员,其实体远在main()函数开始之前就已经在全局数据段中诞生了。其生命期和类对象是异步的,(而且静态语意说明即使没有类实体的存在,其静态数据成员的实体也是存的)这个时候对象的生命期还没有开始,如果你要到类中去初始化类静态数据成员,让静态数据成员的初始化依赖于类的实体,,那怎么满足前述静态语意呢?难道类永远不被实例化,我们就永远不能访问到被初始化的静态数据成员吗? 

3)能在类中初始化的static成员只有一种,那就是静态常量成员。

<span style="font-size:14px;">class A{  private:    static const int count = 0; // 静态常量成员可以在类内初始化};</span>
4)静态成员函数不可以调用类的非静态成员。因为静态成员函数不含this指针。

     静态成员函数不可以同时声明为 virtual、const、volatile函数。

     静态成员是可以独立访问的,也就是说,无须创建任何对象实例就可以访问。


(18)mutable、volatile、=default、delete


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

<span style="font-size:14px;">class ClxTest{ public:  void Output() const;};void ClxTest::Output() const{ cout << "Output for test!" << endl;}void OutputTest(const ClxTest& lx){ lx.Output();}</span>
如果现在,我们要增添一个功能:计算每个对象的输出次数。如果用来计数的变量是普通的变量的话,那么在const成员函数Output里面是不能修改该变量的值的;而该变量跟对象的状态无关,所以应该为了修改该变量而去掉Output的const属性。这个时候,就该我们的mutable出场了——只要用mutalbe来修饰这个变量,所有问题就迎刃而解了。

<span style="font-size:14px;">class ClxTest{ public:  ClxTest();  ~ClxTest();  void Output() const;  int GetOutputTimes() const; private:  mutable int m_iTimes;};ClxTest::ClxTest(){ m_iTimes = 0;}ClxTest::~ClxTest(){}void ClxTest::Output() const{ cout << "Output for test!" << endl; m_iTimes++;}int ClxTest::GetOutputTimes() const{ return m_iTimes;}void OutputTest(const ClxTest& lx){ cout << lx.GetOutputTimes() << endl; lx.Output(); cout << lx.GetOutputTimes() << endl;}</span>
volatile:volatile 影响编译器编译的结果,指出,volatile 变量是随时可能发生变化的,与volatile变量有关的运算,不要进行编译优化,以免出错
。原文

例如: 
volatile int i=10; 
int j = i; 
... 
int k = i; 
volatile 告诉编译器i是随时可能发生变化的,每次使用它的时候必须从i的地址中读取,因而编译器生成的可执行码会重新从i的地址读取数据放在k中。 
而优化做法是,由于编译器发现两次从i读数据的代码之间的代码没有对i进行过操作,它会自动把上次读的数据放在k中。而不是重新从i里面读。这样以来,如果i是一个寄存器变量或者表示一个端口数据就容易出错,所以说volatile可以保证对特殊地址的稳定访问,不会出错。
下面是volatile变量的几个例子: 
1) 并行设备的硬件寄存器(如:状态寄存器) 
2) 一个中断服务子程序中会访问到的非自动变量(Non-automatic variables) 
3) 多线程应用中被几个任务共享的变量 

问题:
1)一个参数既可以是const还可以是volatile吗?解释为什么。 
2); 一个指针可以是volatile 吗?解释为什么。 
3); 下面的函数有什么错误: 
int square(volatile int *ptr) 

return *ptr * *ptr; 

下面是答案: 
1)是的。一个例子是只读的状态寄存器。它是volatile因为它可能被意想不到地改变。它是const因为程序不应该试图去修改它。 
2); 是的。尽管这并不很常见。一个例子是当一个中服务子程序修该一个指向一个buffer的指针时。 
3) 这段代码有点变态。这段代码的目的是用来返指针*ptr指向值的平方,但是,由于*ptr指向一个volatile型参数,编译器将产生类似下面的代码: 
int square(volatile int *ptr) 

int a,b; 
a = *ptr; 
b = *ptr; 
return a * b; 

由于*ptr的值可能被意想不到地该变,因此a和b可能是不同的。结果,这段代码可能返不是你所期望的平方值!正确的代码如下: 
long square(volatile int *ptr) 

int a; 
a = *ptr; 
return a * a; 

位操作(Bit manipulation)

=default:要求编译器生成构造函数或拷贝构造函数。

Sales_data()=default;

Sales_data(const Sales_data &)=default;

其中,=default既可以和声明一起出现在类的内部,也可以作为定义出现在类的外部。=default出现在内部,则默认构造函数是内联的;如果在类的外部,则该成员默认情况下不是内联的。

delete:阻止拷贝。在函数后加上=delete,指出我们希望将他定义为删除的:

NoCopy(const NoCopy&) = delete;//阻止拷贝

析构函数不能是删除的成员。


(19)必须用带有初始化列表的构造函数的情况

初始化和赋值对内置类型的成员没有什么大的区别。对非内置类型成员变量,为了避免两次构造,推荐使用类构造函数初始化列表。但有的时候必须用带有初始化列表的构造函数:


1.成员类型是没有默认构造函数的类。若没有提供显示初始化式,则编译器隐式使用成员类型的默认构造函数,若类没有默认构造函数,则编译器尝试使用默认构造函数将会失败。
2.const成员引用类型的成员因为const对象或引用类型只能初始化,不能对他们赋值。


初始化列表:所有类非静态数据成员都可以在这里初始化, 所有类静态数据成员都不能在这里初始化。


性能:初始化类的成员有两种方式,一是使用初始化列表,二是在构造函数体内进行赋值操作。使用初始化列表主要是基于性能问题,对于内置类型,如int, float等,使用初始化类表和在构造函数体内初始化差别不是很大,但是对于类类型来说,最好使用初始化列表,使用初始化列表少了一次调用默认构造函数的过程,这对于数据密集型的类来说,是非常高效的。因为类类型的数据成员对象在进入函数体前已经构造完成,也就是说在成员初始化列表处进行构造对象的工作,调用构造函数,在进入函数体之后,进行的是对已经构造好的类对象的赋值,又调用个拷贝赋值操作符才能完成(如果并未提供,则使用编译器提供的默认按成员赋值行为)

(20)构造函数调用顺序

基类->子对象->派生类

1、创建派生类的对象,基类的构造函数函数优先被调用(也优先于派生类里的成员类);

2、如果类里面有成员类,成员类的构造函数优先被调用;

3、基类构造函数如果有多个基类则构造函数的调用顺序是某类在类派生表中出现的顺序,而不是它们在成员初始化表中的顺序;


4、成员类对象构造函数如果有多个成员类对象则构造函数的调用顺序是对象在类中被声明的顺序,而不是它们出现在成员初始化表中的顺序;


5、派生类构造函数
  作为一般规则派生类构造函数应该不能直接向一个基类数据成员赋值而是把值传递给适当的基类构造函数否则两个类的实现变成紧耦合的(tightly coupled)将更加难于正确地修改或扩展基类的实现。(基类设计者的责任是提供一组适当的基类构造函数)

(21)

int(*p)[2];------p为指向含2个元素的一维整形数组的指针变量(是指针)

它相当于一个二维数组的用法,只是它是一个n行2列的数组,可以这样来用:

#include <iostream>using namespace std;void main() {int (*p)[2];int b[3][2] = {{1, 2}, {3, 4}, {5, 6}};p = b;for(int i = 0; i < 3; i++) {for(int j = 0; j < 2; j++) //cout << p[i][j]; //cout << *(*(p+i)+j);cout << endl;}

int *p[4];-------定义指针数组p,它由4个指向整型数据的指针元素组成(是数组)

即:p是包含4个元素的指针数组,指针指向的是int型。

#include <iostream>using namespace std;int main(int argc, char* argv[]) {int* p[2];int a[3] = {1, 2, 3};int b[4] = {4, 5, 6, 7};p[0] = a;p[1] = b;for(int i = 0; i < 3; i++)cout << *p[0] + i;// cout << **p + i;cout << endl;for(i = 0; i < 4; i++)cout << *p[1] + i;// cout << **p + i;return 0;}



int(*)[4];--------实际上可以看作是一种数据类型。也就是第一个(int(*p)[4];)中定义的p的数据类型


int *(*p)[4];(*P)表示p是一个指针,然后与右边的【】结合,表示P是指向一个有四个元素的数组,
int *表示返回指向整形的指针,所以int *(*p)[4];表示p指向一个有含有四个指向整形的指针元素的数组

(22)char*转换成int*

int  main(){    char a[10];    a[0] = 0xFF;    a[1] = 0x00;    a[2] = 0x00;    a[3] = 0x00;    char *pa = a;    int *pi = (int *)pa;    int d = *pi;   cout<<d;    return 0;}

输出:255

 

 

 

强转前,char*指针指向的内存地址只是0x0012FF54;

强转后,int*指针指向的内存地址从0x0012FF54到0x0012FF57。

(23)c字符串和c++字符串区别

在C++中则把字符串封装成了一种数据类型string,可以直接声明变量并进行赋值等字符串操作。以下是C字符串和C++中string的区别:

C字符串

string对象C++

所需的头文件名称 

<string><string.h>

<string><string.h>

需要头文件 原因

为了使用字符串函数

为了使用string

声明 方式

char name[20];

string name;

初始化方式

char name[20]="nihao";  

string name = "nihao";

必须声明字符串长度么?

使用一个null字符么?

字符串赋值 的实现方式

strcpy(name,"John");

name = "John";

优点 

更快 

更易于使用,优选方案

可以赋一个比现有字符更长的字符串么?

不能

可以

string对象和C字符串之间的转换:
可以将C字符串存储在string类型的变量中,例如:
char a[] = "nihao";
string b;
b=a;
string对象不能自动的转换为C字符串,需要进行显式的类型转换,需要用到string类的成员函数c_str().
例如:

strcpy(a,b.c_str());

(24) 结构体初始化问题

struct Student{  long id;  char name[20];  char sex;}a= {0};其相当于a.id=0;a.name=“”;a.sex=‘\0x0’。仅仅对其中部分的成员变量进行初始化,要求初始化的数据至少有一个,其他没有初始化的成员变量由系统完成初始化,为其提供缺省的初始化值。各种基本数据类型的成员变量初始化缺省值如下。数据类型 缺省初始化值 Int                   0Char                ‘\0x0’float                0.0double               0.0char Array[n]        ”int Array[n]         {0,0…,0}

(25)C++11多线程

C++11 新标准中引入了四个头文件来支持多线程编程,他们分别是<atomic> ,<thread>,<mutex>,<condition_variable>和<future>。

  • <atomic>:该头文主要声明了两个类, std::atomic 和 std::atomic_flag,另外还声明了一套 C 风格的原子类型和与 C 兼容的原子操作的函数。
  • <thread>:该头文件主要声明了 std::thread 类,另外 std::this_thread 命名空间也在该头文件中。
  • <mutex>:该头文件主要声明了与互斥量(mutex)相关的类,包括 std::mutex 系列类,std::lock_guard, std::unique_lock, 以及其他的类型和函数。
  • <condition_variable>:该头文件主要声明了与条件变量相关的类,包括 std::condition_variable 和 std::condition_variable_any。
  • <future>:该头文件主要声明了 std::promise, std::package_task 两个 Provider 类,以及 std::future 和 std::shared_future 两个 Future 类,另外还有一些与之相关的类型和函数,std::async() 函数就声明在此头文件中。
#include <thread>#include <iostream>void hello(){   std::cout << "Hello from thread " << std::endl;}int main(){    std::thread t1(hello);    t1.join();    std::cout<<"Main Thread"<<std::endl;    return 0;}


通过thread 类直接申明一个线程t1,参数是这个线程执行的回调函数的地址,通过join()方法阻塞主线程,直到t1线程执行结束为止。

(26)C++的四种强制类型转换

C++的四种强制类型转换,所以C++不是类型安全的。分别为:static_cast , dynamic_cast , const_cast , reinterpret_cast

static_cast:可以实现C++中内置基本数据类型之间的相互转换。

如果涉及到类的话,static_cast只能在有相互联系的类型中进行相互转换,不一定包含虚函数

class A{};class B:public A{};class C{}; int main(){    A* a=new A;    B* b;    C* c;    b=static_cast<B>(a);  // 编译不会报错, B类继承A类    c=static_cast<B>(a);  // 编译报错, C类与A类没有任何关系    return 1;}


const_cast: const_cast操作不能在不同的种类间转换。相反,它仅仅把一个它作用的表达式转换成常量。它可以使一个本来不是const类型的数据转换成const类型的,或者把const属性去掉。

const_cast<type_id> (expression)

reinterpret_cast: 有着和C风格的强制转换同样的能力。它可以转化任何内置的数据类型为其他任何的数据类型也可以转化任何指针类型为其他的类型。它甚至可以转化内置的数据类型为指针,无须考虑类型安全或者常量的情形。不到万不得已绝对不用。

dynamic_cast: 

(1)其他三种都是编译时完成的,dynamic_cast是运行时处理的,运行时要进行类型检查。

(2)不能用于内置的基本数据类型的强制转换。

(3)dynamic_cast转换如果成功的话返回的是指向类的指针或引用,转换失败的话则会返回NULL。

(4)使用dynamic_cast进行转换的,基类中一定要有虚函数,否则编译不通过。

        B中需要检测有虚函数的原因

        这是由于运行时类型检查需要运行时类型信息,而这个信息存储在类的虚函数表(关于虚函数表的概念,详细可见<Inside c++ object model>)中,

        只有定义了虚函数的类才有虚函数表。

 (5)在类的转换时,在类层次间进行上行转换时,dynamic_cast和static_cast的 效果是一样的。在进行下行转换时,dynamic_cast具有类型检查的功能,比               static_cast更安全。向上转换即为指向子类对象的向下转换,即将父类指针转化子类指针。向下转换的成功与否还与将要转换的类型有关,即要转换的 指针指向的对象的实际类型与转换以后的对象类型一定要相同,否则转换失败。

#include<iostream>#include<cstring>using namespace std;class A{   public:   virtual void f()   {       cout<<"hello"<<endl;       };}; class B:public A{    public:    void f()    {        cout<<"hello2"<<endl;        }; }; class C{  void pp()  {      return;  }}; int fun(){    return 1;}int main(){    A* a1=new B;//a1是A类型的指针指向一个B类型的对象    A* a2=new A;//a2是A类型的指针指向一个A类型的对象    B* b;    C* c;    b=dynamic_cast<B*>(a1);//结果为not null,向下转换成功,a1之前指向的就是B类型的对象,所以可以转换成B类型的指针。    if(b==NULL)    {        cout<<"null"<<endl;    }    else    {        cout<<"not null"<<endl;    }    b=dynamic_cast<B*>(a2);//结果为null,向下转换失败    if(b==NULL)    {        cout<<"null"<<endl;    }    else    {        cout<<"not null"<<endl;    }    c=dynamic_cast<C*>(a);//结果为null,向下转换失败    if(c==NULL)    {        cout<<"null"<<endl;    }    else    {        cout<<"not null"<<endl;    }    delete(a);    return 0;}

(27)虚函数&纯虚函数


概念:
定义一个函数为虚函数,不代表函数为不被实现的函数。
定义他为虚函数是为了允许用基类的指针来调用子类的这个函数。
定义一个函数为纯虚函数,才代表函数没有被实现。

定义纯虚函数是为了实现一个接口,起到一个规范的作用,规范继承这个类的程序员必须实现这个函数。




一、定义
 纯虚函数是在基类中声明的虚函数,它在基类中没有定义,但要求任何派生类都要定义自己的实现方法。在基类中实现纯虚函数的方法是在函数原型后加“=0”
 virtual void funtion1()=0
二、引入原因
  1、为了方便使用多态特性,我们常常需要在基类中定义虚拟函数。
  2、在很多情况下,基类本身生成对象是不合情理的。例如,动物作为一个基类可以派生出老虎、孔雀等子类,但动物本身生成对象明显不合常理。
  为了解决上述问题,引入了纯虚函数的概念,将函数定义为纯虚函数(方法:virtual ReturnType Function()= 0;),则编译器要求在派生类中必须予以重写以实现多态性。同时含有纯虚拟函数的类称为抽象类,它不能生成对象。这样就很好地解决了上述两个问题。

声明了纯虚函数的类是一个抽象类。所以,用户不能创建类的实例,只能创建它的派生类的实例。
纯虚函数最显著的特征是:它们必须在继承类中重新声明函数(不要后面的=0,否则该派生类也不能实例化),而且它们在抽象类中往往没有定义。
定义纯虚函数的目的在于,使派生类仅仅只是继承函数的接口。
纯虚函数的意义,让所有的类对象(主要是派生类对象)都可以执行纯虚函数的动作,但类无法为纯虚函数提供一个合理的缺省实现。所以类纯虚函数的声明就是在告诉子类的设计者,“你必须提供一个纯虚函数的实现,但我不知道你会怎样实现它”。

 
总结:
1、纯虚函数声明如下: virtual void funtion1()=0;
纯虚函数一定没有定义,纯虚函数用来规范派生类的行为,即接口。包含纯虚函数的类是抽象类抽象类不能定义实例,但可以声明指向实现该抽象类的具体类的指针或引用。
2、虚函数声明如下:virtual ReturnType FunctionName(Parameter);虚函数必须实现,如果不实现,编译器将报错,错误提示为:
error LNK****: unresolved external symbol "public: virtual void __thiscall ClassName::virtualFunctionName(void)"
3、对于虚函数来说,父类和子类都有各自的版本。由多态方式调用的时候动态绑定。
4、实现了纯虚函数的子类,该纯虚函数在子类中就编程了虚函数,子类的子类即孙子类可以覆盖该虚函数,由多态方式调用的时候动态绑定。
5、虚函数是C++中用于实现多态(polymorphism)的机制。核心理念就是通过基类访问派生类定义的函数。
6、在有动态分配堆上内存的时候,析构函数必须是虚函数,但没有必要是纯虚的。
7、友元不是成员函数,只有成员函数才可以是虚拟的,因此友元不能是虚拟函数。但可以通过让友元函数调用虚拟成员函数来解决友元的虚拟问题。
8、析构函数应当是虚函数,将调用相应对象类型的析构函数,因此,如果指针指向的是子类对象,将调用子类的析构函数,然后自动调用基类的析构函数。


(28)虚继承的作用是什么?
在多继承中,子类可能同时拥有多个父类,如果这些父类还有相同的父类(祖先类),那么在子类中就会有多份祖先类。例如,类B和类C都继承与类A,如果类D派生于B和C,那么类D中就会有两份A。为了防止在多继承中子类存在重复的父类情况,可以在父类继承时使用虚函数,即在类B和类C继承类A时使用virtual关键字,例如:
class B : virtual public A
class C : virtual public A
注:因为多继承会带来很多复杂问题,因此要慎用。

(29)进程和线程的区别and联系

进程,是并发执行的程序在执行过程中分配和管理资源的基本单位,是一个动态概念,竟争计算机系统资源的基本单位。每一个进程都有一个自己的地址空间,即进程空间或(虚空间)。进程空间的大小 只与处理机的位数有关,一个 16 位长处理机的进程空间大小为 216 ,而 32 位处理机的进程空间大小为 232 。进程至少有 5 种基本状态,它们是:初始态,执行态,等待状态,就绪状态,终止状态。

        线程,在网络或多用户环境下,一个服务器通常需要接收大量且不确定数量用户的并发请求,为每一个请求都创建一个进程显然是行不通的,——无论是从系统资源开销方面或是响应用户请求的效率方面来看。因此,操作系统中线程的概念便被引进了。线程,是进程的一部分,一个没有线程的进程可以被看作是单线程的。线程有时又被称为轻权进程或轻量级进程,也是 CPU 调度的一个基本单位。

简而言之,一个程序至少有一个进程,一个进程至少有一个线程。
 

关系

一个线程可以创建和撤销另一个线程;同一个进程中的多个线程之间可以并发执行.

相对进程而言,线程是一个更加接近于执行体的概念,它可以与同进程中的其他线程共享数据,但拥有自己的栈空间,拥有独立的执行序列


区别

进程和线程的主要差别在于它们是不同的操作系统资源管理方式。进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。但对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程。


进程与线程的区别:

    (1)调度:线程作为调度和分配的基本单位,进程作为拥有资源的基本单位

    (2)并发性:不仅进程之间可以并发执行,同一个进程的多个线程之间也可并发执行

    (3)拥有资源:进程是拥有资源的一个独立单位,线程不拥有系统资源,但可以访问隶属于进程的资源.

    (4)系统开销:在创建或撤消进程时,由于系统都要为之分配和回收资源,导致系统的开销明显大于创建或撤消线程时的开销。

本质区别:
进程拥有独立资源,线程共享除堆栈外的所有资源

(29) 智能指针
http://blog.csdn.net/worldwindjp/article/details/18843087

     1,你知道智能指针吗?智能指针的原理。
     2,常用的智能指针。
     3,智能指针的实现。

  1答案:智能指针是一个类,这个类的构造函数中传入一个普通指针,析构函数中释放传入的指针。智能指针的类都是栈上的对象,所以当函数(或程序)结束时会自动被释放,

     2, 最常用的智能指针: 

              1)std::auto_ptr,有很多问题。 不支持复制(拷贝构造函数)和赋值(operator =),但复制或赋值的时候不会提示出错。因为不能被复制,所以不能被放入容器中。

              2) C++11引入的unique_ptr, 也不支持复制和赋值,但比auto_ptr好,直接赋值会编译出错。实在想赋值的话,需要使用:std::move。

               例如:

                    std::unique_ptr<int> p1(new int(5));
                    std::unique_ptr<int> p2 = p1; // 编译会出错
                    std::unique_ptr<int> p3 = std::move(p1); // 转移所有权, 现在那块内存归p3所有, p1成为无效的指针.

              3) C++11或boost的shared_ptr,基于引用计数的智能指针。可随意赋值,直到内存的引用计数为0的时候这个内存会被释放。

              4)C++11或boost的weak_ptr,弱引用。 引用计数有一个问题就是互相引用形成环,这样两个指针指向的内存都无法释放。需要手动打破循环引用或使用weak_ptr。顾名思义,weak_ptr是一个弱引用,只引用,不计数。如果一块内存被shared_ptr和weak_ptr同时引用,当所有shared_ptr析构了之后,不管还有没有weak_ptr引用该内存,内存也会被释放。所以weak_ptr不保证它指向的内存一定是有效的,在使用之前需要检查weak_ptr是否为空指针。

     3, 智能指针的实现

      下面是一个基于引用计数的智能指针的实现,需要实现构造,析构,拷贝构造,=操作符重载,重载*-和>操作符。

template <typename T>class SmartPointer {public://构造函数SmartPointer(T* p=0): _ptr(p), _reference_count(new size_t){if(p)*_reference_count = 1; else*_reference_count = 0; }//拷贝构造函数SmartPointer(const SmartPointer& src) {if(this!=&src) {_ptr = src._ptr;_reference_count = src._reference_count;(*_reference_count)++;}}//重载赋值操作符SmartPointer& operator=(const SmartPointer& src) {if(_ptr==src._ptr) {return *this;}releaseCount();_ptr = src._ptr;_reference_count = src._reference_count;(*_reference_count)++;return *this;}//重载操作符T& operator*() {if(ptr) {return *_ptr;}//throw exception}//重载操作符T* operator->() {if(ptr) {return _ptr;}//throw exception}//析构函数~SmartPointer() {if (--(*_reference_count) == 0) {            delete _ptr;            delete _reference_count;        }}private:T *_ptr;        size_t *_reference_count;        void releaseCount() {if(_ptr) {(*_reference_count)--;    if((*_reference_count)==0) {    delete _ptr;    delete _reference_count;    }}    }};int main() {    SmartPointer<char> cp1(new char('a'));    SmartPointer<char> cp2(cp1);    SmartPointer<char> cp3;    cp3 = cp2;    cp3 = cp1;    cp3 = cp3;    SmartPointer<char> cp4(new char('b'));    cp3 = cp4;}


(30)const char* 和string 转换

(1) const char*转换为 string,直接赋值即可。

     EX: const char* tmp = "tsinghua".

            string s = tmp;

(2) string转换为const char*,利用c_str()

    EX:  string s = "tsinghua";

           const char*tmp = s.c_str();

2. char*和const char*之间的转换

(1) const char*转化为char*,利用const_cast<char*>

     EX: const char* tmp = "tsinghua";

             char* p = const_cast<char*>(tmp);

(2) char*转化为const char*,直接赋值即可。

     char* p = "tsinghua".

     const char* tmp = p;

3. char*和string之间的转换

  有了1和2的基础,char*和string转化就很简单了。

(1)char*转化为string,直接赋值即可。

     EX: char* p = "tsinghua".

            string str = p;

(2)string转化为char*,走两步,先是string->const char*,然后是const char*->char*

     EX:  string str = "tsinghua";

             char* p = const_cast<char*>(str.c_str()):


(31)C++组合

在一个类中以另一个类的对象作为数据成员的,称为类的组合

类的组合和继承一样,是软件重用的重要方式。组合和继承都是有效地利用已有类的资源。但二者的概念和用法不同。通过继承建立了派生类与基类的关系,它是一种 “是”的关系,如“白猫是猫”,“黑人是人”,派生类是基类的具体化实现,是基类中的一 种。通过组合建立了成员类与组合类(或称复合类)的关系,在本例中BirthDate是成员类,Professor是组合类(在一个类中又包含另一个类的对象成员)。它们之间不是‘‘是”的 关系,而是“有”的关系。不能说教授(Professor)是一个生日(BirthDate),只能说教授(Professor)有一个生日(BirthDate)的属性。

Professor类通过继承,从Teacher类得到了num,name,age,sex等数据成员,通过组合,从BirthDate类得到了year,month,day等数据成员。继承是纵向的,组合是横向的。

在C++中,所谓“继承”就是在一个已存在的类的基础上建立一个新的类。已存在的类称为“基类(base class)”或“父类(father class)”,新建的类称为“派生类(derived class)”或“子类(son class )”。

一个新类从已有的类那里获得其已有特性,这种现象称为类的继承。通过继承,一个新建子类从已有的父类那里获得父类的特性。从另一角度说,从已有的类(父类)产生一个新的子类,称为类的派生。类的继承是用已有的类来建立专用类的编程技术。派生类继承了基类的所有数据成员和成员函数,并可以对成员作必要的增加或调整。一个基类可以派生出多个派生类,每一个派生类又可以作为基类再派生出新的派生类,因此基类和派生类是相对而言的。一代一代地派生下去,就形成类的继承层次结构。相当于一个大的家族,有许多分支,所有的子孙后代都继承了祖辈的基本特征,同时又有区别和发展。与之相仿,类的每一次派生,都继承了其基类的基本特征,同时又根据需要调整和扩充原 有的特征。

适用原则:

无论什么时候,组合应比继承优先考虑,因为继承有损松散耦合度。

组合是说has a,比如树有树叶,树has a 树叶
继承是说is a,比如梧桐树是树,梧桐树 is a 树
现在假设,A类要使用B类实现一些操作;此时,可以使用私有继承和组合,那么到底选用哪个呢?
标准答案是优先使用组合,因为它具有更好的封装性,应变能力强。
但是,当B涉及多态或其它特殊理由的时候(比如,B是一个结构,并明确要求需要对A进行B的切片),就必须使用私有继承。

(32)菱形继承

先看一下菱形继承长什么样。

 

B和C从A中继承,而D多重继承于B,C。那就意味着D中会有A中的两个拷贝。因为成员函数不体现在类的内存大小上,所以实际上可以看到的情况是D的内存分布中含有2组A的成员变量。

C++中一个空类的大小为什么是1?


那是被编译器插进去的一个char ,使得这个class的不同实体(object)在内存中配置独一无二的地址。
也就是说这个char是用来标识类的不同对象的
宏和内联函数的主要区别如下:
        1. 宏是代码处不加任何验证的简单替代,而内联函数是将代码直接插入调用处,而减少了普通函数调用时的资源消耗。
        2. 宏不是函数,只是在编译前预处理阶段将程序中有关字符串替换成宏体。
        3. inline是函数,但在编译中不单独产生代码,而是将有关代码嵌入到调用处。


stl容器区别: vector list deque set map 


在STL中基本容器有: vector、list、deque、set、map

set 和map都是无序的保存元素,只能通过它提供的接口对里面的元素进行访问

set:集合, 用来判断某一个元素是不是在一个组里面,使用的比较少
map:映射,相当于字典,把一个值映射成另一个值,如果想创建字典的话使用它好了
底层采用的是树型结构,多数使用平衡二叉树实现,查找某一值是常数时间,遍历起来效果也不错, 只是每次插入值的时候,会重新构成底层的平衡二叉树,效率有一定影响.

vector、list、deque是有序容器
1.vector
vector就是动态数组.它也是在堆中分配内存,元素连续存放,有保留内存,如果减少大小后,内存也不会释放.如果新值>当前大小时才会再分配内存.

它拥有一段连续的内存空间,并且起始地址不变,因此它能非常好的支持随即存取,即[]操作符,但由于它的内存空间是连续的,所以在中间进行插入和删除会造成内存块的拷贝,另外,当该数组后的内存空间不够时,需要重新申请一块足够大的内存并进行内存的拷贝。这些都大大影响了vector的效率。

对最后元素操作最快(在后面添加删除最快 ), 此时一般不需要移动内存,只有保留内存不够时才需要


对中间和开始处进行添加删除元素操作需要移动内存,如果你的元素是结构或是类,那么移动的同时还会进行构造和析构操作,所以性能不高 (最好将结构或类的指针放入vector中,而不是结构或类本身,这样可以避免移动时的构造与析构)。
访问方面,对任何元素的访问都是O(1),也就是是常数的,所以vector常用来保存需要经常进行随机访问的内容,并且不需要经常对中间元素进行添加删除操作.

相比较可以看到vector的属性与string差不多,同样可以使用capacity看当前保留的内存,使用swap来减少它使用的内存.

capacity()返回vector所能容纳的元素数量(在不重新分配内存的情况下)      测试push_back  1000个数据  capacity返回16384


总结
需要经常随机访问请用vector

2.list
list就是双向链表,元素也是在堆中存放,每个元素都是放在一块内存中,它的内存空间可以是不连续的,通过指针来进行数据的访问,这个特点使得它的随机存取变的非常没有效率,因此它没有提供[]操作符的重载。但由于链表的特点,它可以以很好的效率支持任意地方的删除和插入。

list没有空间预留习惯,所以每分配一个元素都会从内存中分配,每删除一个元素都会释放它占用的内存.

list在哪里添加删除元素性能都很高,不需要移动内存,当然也不需要对每个元素都进行构造与析构了,所以常用来做随机操作容器.
但是访问list里面的元素时就开始和最后访问最快
访问其它元素都是O(n) ,所以如果需要经常随机访问的话,还是使用其它的好

总结
如果你喜欢经常添加删除大对象的话,那么请使用list
要保存的对象不大,构造与析构操作不复杂,那么可以使用vector代替
list<指针>完全是性能最低的做法,这种情况下还是使用vector<指针>好,因为指针没有构造与析构,也不占用很大内存

3.deque
deque是一个双端队列(double-ended queue),也是在堆中保存内容的.它的保存形式如下:
[堆1]
...
[堆2]
...
[堆3]
每个堆保存好几个元素,然后堆和堆之间有指针指向,看起来像是list和vector的结合品.

它支持[]操作符,也就是支持随即存取,可以让你在前面快速地添加删除元素,或是在后面快速地添加删除元素,然后还可以有比较高的随机访问速度,和vector的效率相差无几,它支持在两端的操作:push_back,push_front,pop_back,pop_front等,并且在两端操作上与list的效率也差不多。
在标准库中vector和deque提供几乎相同的接口,在结构上它们的区别主要在于这两种容器在组织内存上不一样,deque是按页或块来分配存储器的,每页包含固定数目的元素.相反vector分配一段连续的内存,vector只是在序列的尾段插入元素时才有效率,而deque的分页组织方式即使在容器的前端也可以提供常数时间的insert和erase操作,而且在体积增长方面也比vector更具有效率

总结:
vector是可以快速地在最后添加删除元素,并可以快速地访问任意元素
list是可以快速地在所有地方添加删除元素,但是只能快速地访问最开始与最后的元素
deque在开始和最后添加元素都一样快,并提供了随机访问方法,像vector一样使用[]访问任意元素,但是随机访问速度比不上vector快,因为它要内部处理堆跳转
deque也有保留空间.另外,由于deque不要求连续空间,所以可以保存的元素比vector更大,这点也要注意一下.还有就是在前面和后面添加元素时都不需要移动其它块的元素,所以性能也很高。

因此在实际使用时,如何选择这三个容器中哪一个,应根据你的需要而定,一般应遵循下面
的原则:
1、如果你需要高效的随即存取,而不在乎插入和删除的效率,使用vector
2、如果你需要大量的插入和删除,而不关心随即存取,则应使用list
3、如果你需要随即存取,而且关心两端数据的插入和删除,则应使用deque。


1 0