C++基础知识总结

1. inline
为了解决一些频繁调用的小函数大量消耗栈空间的问题,特别引入了inline修饰符,表示内联函数。
Inline仅仅是对编译器的建议,并不是申明了内联就会内联。
内联函数的定义需要放在头文件中实现。
定义在类中的成员函数缺省都是内联的。
关键字inline必须与函数定义体放在一起才能使函数成为内联,仅将inline放在函数声明前不起任何作用。Inline是用于实现的关键字。
使用限制:
Inline只适合函数体代码简单的函数使用,不能包含复杂的结构控制语句,例如while,switch,并且内联函数本身不能直接递归。
慎用inline:
内联是以代码膨胀为代价,仅仅省去了函数调用的开销,从而提高了函数的执行效率。如果执行函数体内代码的时间相比于函数调用的开销大,那么效率收获会很少。另一方面,每一处内联函数的调用都要复制代码,将使程序的总代码量增大,消耗更多的内存空间。
以下情况不宜使用内联:
1.如果函数体内的代码量比较长,使用内联将导致内存消耗代价较高。
2.如果函数体内出现循环,那么执行函数体内代码的时间要比函数调用开销大。
小结:
内联函数并不是一个增强性能的灵丹妙药。只有当函数非常短小的时候它才能得到我们想要的效果;但是,如果函数并不是很短,而且在很多地方都调用的话,那么将会使得可执行体的体积增大。
2. const
const是C++中常用的类型修饰符,可修饰变量,参数,返回值,甚至函数体。const可以提高程序的健壮性。
const修饰参数
 如果输入参数是指针型,用const修饰可以防止指针被意外修改。
 如果参数采用值传递的方式,无需const,因为函数 自动产生临时变量复制该参数。
 非内部数据类型的参数,需要临时对象复制参数,而临时对象的构造,析构,复制较为费时,因此建议采用前加const的引用方式传递非内部数据类型。而内部数据类型无需引用传递。
const修饰函数返回值
 函数返回const指针,表示该指针不能被改动,只能把该指针赋值给const修饰的同类型指针变量。
 函数返回值为值传递,函数会把返回值赋给外部临时变量,用const无意义!不管是内部还是非内部数据类型。
 函数采用引用方式返回的场合不多,只出现在类的赋值函数中,目的是为了实现链式表达。
const修饰成员函数
 表示该成员函数不能修改任何成员变量,并且也不能调用任何修改成员变量的函数。
const修饰变量
 const char *p 表示 指向的内容不能改变;
 char * const p,就是将P声明为常指针,它的地址不能改变,是固定的,但是它的内容可以改变。
 const double pi = 3.14159;
const double *const pi_ptr = π这种const指针是前两种的结合,使得指向的内容和地址都不能发生变化.

const类型转换为非const类型方法
采用const_cast进行转换
用法:const_cast(expression)
小结:
 在编程中要尽可能多的使用const,这样可以获得编译器的帮助,以便写出健壮性的代码;
 要避免最一般的赋值操作,如将const变量赋值;
 在参数中使用const应该使用引用或指针,而不是一把你的对象实例;
 const在成员函数中的三种用法要很好的使用(参数,返回值,函数);
 不要轻易的将函数返回值类型定为const
 任何不会修改数据成员的函数都应该声明为const 类型
3. Volatile
其实不只是“内嵌汇编操纵栈”这种方式属于编译无法识别的变量改变,另外更多的可能是多线程并发访问共享变量时,一个线程改变了变量的值,怎样让改变后的值对其它线程 visible。一般说来,volatile用在如下的几个地方:
1) 中断服务程序中修改的供其它程序检测的变量需要加volatile;
2) 多任务环境下各任务间共享的标志应该加volatile;
3) 存储器映射的硬件寄存器通常也要加volatile说明,因为每次对它的读写都可能由不同意义;
4. const和static的区别
cosnt成员函数主要目的是防止成员函数修改对象的内容。即const成员函数不能修改成员变量的值,但可以访问成员变量。当方法成员函数时,该函数只能是const成员函数。
static成员函数主要目的是作为类作用域的全局函数。不能访问类的非静态数据成员。类的静态成员函数没有this指针,这导致:1、不能直接存取类的非静态成员变量,调用非静态成员函数2、不能被声明为virtual
5. 空指针野指针
空指针不指向任何对象,在试图使用一个指针之前可以首先检查它是否为空。
野指针不是空指针,是指向“垃圾”内存(不可用内存)的指针。
 指针变量没有被初始化
任何指针变量刚被创建时不会自动成为nullptr指针,它的缺省值是随机的,它会乱指一气。所以,指针变量在创建的同时应当被初始化,要么将指针设置为nullptr,要么让它指向合法的内存。
 指针p被free或者delete之后,没有置为nullptr。这样做会让人误以为p是个合法的指针。
free和delete只是把指针所指向的内存给释放掉,但并没有把指针本身给清理掉。 这时候的指针依然指向原来的位置,只不过这个位置的内存数据已经被销毁。通常会 用语句if (p != nullptr)进行防错处理。很遗憾,此时if语句起不到防错作用,因为 即便p不是nullptr指针,它也不指向合法的内存块。所以在指针指向的内存被释 放后,应该将指针置为nullptr。
 指针操作超越了变量的作用范围。
(a)由于C/C++中指针有++操作,因而在执行该操作的时候,稍有不慎,就容易 指针访问越界,访问了一个不该访问的内存,结果程序崩溃;
(b)另一种情况是指针指向一个临时变量的引用,当该变量被释放时,此时的指针 就变成了一个野指针
小结:
一个铁的纪律,彻底杜绝野指针delete了一个指向动态对象的指针后,及时置为nullptr。相应的,对指针进行解除和引用前判断指针是否为nullptr。

6. 堆和栈的区别
栈区(stack):由编译器自动分配释放,存放函数的参数值,局部变量等。其操作方式类似于数据结构中的栈。栈由系统自动分配。只要栈的剩余空间大于所申请的控件,系统将为程序提供内存,否则将报异常提示栈溢出。
堆区(heap):一般有程序员分配释放(malloc/free,new/delete),若程序员不释放,程序结束时可能由操作系统回收,它与数据结构中的堆是两回事,分配方式类似于链表。堆需要程序员自己申请,并指明大小。
操作系统有一个记录空闲内存地址的链表,当系统感受到程序的申请时,会遍历该链表,寻找第一个空间大于所要申请的控件的堆结点,然后将该节点从空闲结点链表中删除,并将该节点的空间分配给程序。另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样代码中的delete语句才能正确释放本内存控件。由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入到空闲链表中。
申请效率比较:
栈由系统自动分配,速度较快,程序员无法控制
堆是由malloc/new分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来方便。
堆和栈中的存储内容:(内存管理)
栈:在函数调用时,第一个进栈的是主函数中的下一条指令的地址,然后是函数的各个参数,在大多数的C编译器中,参数是由右往左入栈的,然后是函数中的局部变量。注意静态变量是不如栈的。当本次函数调用结束后,局部变量先出栈,然后是参数,最后是栈顶指针指向最开始寸的地址,也就是函数中的下一条指令,程序由该点继续运行。
堆:一般在堆的头部用一个字节存放堆大小。对中具体内容由程序员安排。
全局区(static):全局变量和静态变量的存储是放在一起的,初始化的全局变量和静态变量在一块区域,未初始化的全局比那里和未初始化的静态变量在相邻的另一块区域,程序结束后由系统释放。
文字常量区:常量字符串就放在文字长两驱,程序结束有系统释放。
程序代码区:存放函数体的二进制代码。

小结:
堆和栈的区别可以用一个比喻形象表示:
使用栈就像我们去饭馆吃饭,只管点菜(发出申请)、付钱和吃(使用),吃饱了就走,不必理会切菜,洗菜等准备工作和洗碗,刷锅等扫尾工作,好处是快捷,但自由度小。
使用堆就像我们自己动手做饭,比较麻烦,前期准备工作,后期扫尾工作都需要做,但是比较符合自己的胃口,自由度大。
栈的空间大小一般是有限定的2M。栈不够用的情况一般是程序分配了大量的数组和递归函数太深。当一个函数调用玩返回后他会释放该函数中所以的栈空间。栈是由编译器自动管理的,不用操心。堆是东塔分配内存的,并且可以分配使用很大的内存,但是用不好可能产生内存泄露。并且频繁使用malloc和free会产生内存碎片。

1)空间大小:栈的内存空间是连续的,空间大小通常是系统预先规定好的,即栈顶地址和最大空间是确定的;而堆得内存空间是不连续的,由一个记录空间空间的链表负责管理,因此内存空间几乎没有限制,在32位系统下,内存空间大小可达到4G
2)管理方式:栈由编译器自动分配和释放,而堆需要程序员来手动分配和释放,若忘记delete,容易产生内存泄漏。
3)生长方向不同:对于栈,他是向着内存地址减小的方向生长的,这也是为什么栈的内存空间是有限的;而堆是向着内存地址增大的方向生长的
4)碎片问题:由于栈的内存空间是连续的,先进后出的方式保证不会产生零碎的空间;而堆分配方式是每次在空闲链表中遍历到第一个大于申请空间的节点,每次分配的空间大小一般不会正好等于申请的内存大小,频繁的new操作势必会产生大量的空间碎片
5)分配效率:栈属于机器系统提供的数据结构,计算机会在底层对栈提供支持,出栈进栈由专门的指令执行,因此效率较高。而堆是c/c++函数库提供的,当申请空间时需要按照一定的算法搜索足够大小的内存空间,当没有足够的空间时,还需要额外的处理,因此效率较低。

7. gcc编译原理
c/c++编译器是集成的,编译一般分为四个步骤:
1. 预处理(preprocessing)————–cpp / gcc -E
2. 编译(compilation)—————–cc1 / gcc -s
3. 汇编(assembly)—————as
4. 连接(linking)—————–ld

注意:Gcc指令的一般格式为:Gcc [选项] 要编译的文件 [选项] [目标文件]
其中,目标文件可缺省,Gcc默认生成可执行的文件,命为:编译文件.out

    #include<stdio.h>
    int main()
    {
        printf("Hello! This is our embedded world!\n");

        return 0;
    }
  1. 预处理 Gcc –E hello.c –o hello.i
    在该阶段,编译器将上述代码中的stdio.h编译进来,并且用户可以使用Gcc的选项”-E”进行查看,该选项的作用是让Gcc在预处理结束后停止编译过程。预处理阶段主要处理#include和#define,它把#include包含进来的.h 文件插入到#include所在的位置,把源程序中使用到的用#define定义的宏用实际的字符串代替,我们可以用-E选项要求gcc只进行预处理而不进行后面的三个阶段,
  2. 编译 Gcc –S hello.i –o hello.s
    在这个阶段中,Gcc首先要检查代码的规范性、是否有语法错误等,以确定代码的实际要做的工作,在检查无误后,Gcc把代码翻译成汇编语言。用户可以使用”-S”选项来进行查看,该选项只进行编译而不进行汇编,生成汇编代码。

编译阶段是最重要的阶段,在这个阶段GCC首先检查语法然后把由上步生成的.i编译成.s文件。我们可以用如下命令告诉gcc进行这一步处理,gcc -S hello.i -o hello.s,-S选项告诉gcc把hello.i编译成.s文件;
3. 汇编阶段 Gcc –c hello.s –o hello.o
汇编阶段把.s文件翻译成二进制机器指令文件.o,如命令gcc -c hello.s -o hello.o,其中-c告诉gcc进行汇编处理。这步生成的文件是二进制文件,直接用文本工具打开看到的将是乱码,我们需要反汇编工具如GDB的帮助才能读懂它;
这个阶段接收.c, .i, .s的文件都没有问题。比如gcc -c hello.i -o hello.o等
4. 链接 Gcc hello.o –o hello
在成功编译之后,就进入了链接阶段。在这里涉及到一个重要的概念:函数库。
读者可以重新查看这个小程序,在这个程序中并没有定义”printf”的函数实现,且在预编译中包含进的”stdio.h”中也只有该函数的声明,而没有定义函数的实现,那么,是在哪里实现”printf”函数的呢?最后的答案是:系统把这些函数实现都被做到名为libc.so.6的库文件中去了,在没有特别指定时,Gcc会到系统默认的搜索路径”/usr/lib”下进行查找,也就是链接到libc.so.6库函数中去,这样就能实现函数”printf”了,而这也就是链接的作用。
编译原理
编译流程包括 词法分析,语法分析,语义分析,中间代码,优化,目标代码
8. 内存管理方式
栈:局部变量,函数参数存储在该区,由编译器自动分配和释放。

堆(Heap):需要程序员手动分配和释放(new,delete),属于动态分配方式。内存空间几乎没有限制,内存空间不连续,因此会产生内存碎片。操作系统有一个记录空间内存的链表,当收到内存申请时遍历链表,找到第一个空间大于申请空间的堆节点,将该节点分配给程序,并将该节点从链表中删除。一般,系统会在该内存空间的首地址处记录本次分配的内存大小,用于delete释放该内存空间。

全局/静态存储区:全局变量,静态变量分配到该区,到程序结束时自动释放,包括DATA段(全局初始化区)与BBS段(全局未初始化段)。其中,初始化的全局变量和静态变量存放在DATA段,未初始化的全局变量和静态变量存放在BBS段。BBS段特点:在程序执行前BBS段自动清零,所以未初始化的全局变量和静态变量在程序执行前已经成为0.

文字常量区:存放常量,而且不允许修改。程序结束后由系统释放。

程序代码区:存放程序的二进制代码

9. new和malloc

10. extern “C”
extern “C”的主要作用就是为了能够正确实现C++代码调用其他C语言代码。加上extern “C”后,会指示编译器这部分代码按C语言的进行编译,而不是C++的。由于C++支持函数重载,因此编译器编译函数的过程中会将函数的参数类型也加到编译后的代码中,而不仅仅是函数名;而C语言并不支持函数重载,因此编译C语言代码的函数时不会带上函数的参数类型,一般只包括函数名。
主要作用:
1、C++代码调用C语言代码
2、在C++的头文件中使用
3、在多个人协同开发时,可能有的人比较擅长C语言,而有的人擅长C++,这样的 情况下也会有用到
使用要点:
1. 可以是单一语句
extern “C” double sqrt(double);
2. 可以是复合语句, 相当于复合语句中的声明都加了extern “C”
extern “C”
{
double sqrt(double);
int min(int, int);
}

3.可以包含头文件,相当于头文件中的声明都加了extern “C”
extern “C”
{
#i nclude
}
4. 不可以将extern “C” 添加在函数内部
5. 如果函数有多个声明,可以都加extern “C”, 也可以只出现在第一次声明中,后面的声明会接受第一个链接指示符的规则。
6. 除extern “C”, 还有extern “FORTRAN” 等。

11. 如何避免哈希值重复问题
拉链法
拉出一个动态链表代替静态顺序存储结构,可以避免哈希函数的冲突,不过缺点就是链表的设计过于麻烦,增加了编程复杂度。此法可以完全避免哈希函数的冲突。
开放定址法
开放地址法有一个公式:Hi=(H(key)+di) MOD m i=1,2,…,k(k<=m-1)
其中,m为哈希表的表长。di 是产生冲突的时候的增量序列。如果di值可能为1,2, 3,…m-1,称线性探测再散列。
如果di取1,则每次冲突之后,向后移动1个位置.如果di取值可能为1,-1,4,-4,9,-9,1 6,-16,…k*k,-k*k(k<=m/2)
称二次探测再散列。如果di取值可能为伪随机数列。称伪随机探测再散列。
建域法
假设哈希函数的值域为[0,m-1],则设向量HashTable[0..m-1]为基本表,另外设立存储空间向量OverTable[0..v]用以存储发生冲突的记录
12. string实现
构造函数,复制构造函数,析构函数,赋值函数关键部分

因为String里涉及动态内存的管理,默认的拷贝构造函数在运行时只会进行浅复制,即只复制内存区域的指针,会造成两个对象指向同一块内存区域的现象。如果一个对象销毁或改变了该内存区域,会造成另一个对象运行或者逻辑上出错。这时就要求程序员自己实现这些函数进行深复制,即不止复制指针,需要连同内存的内容一起复制。

除了以上四个必须的函数,这里还实现了一些附加的内容。
• 若干个运算符重载,这里的几个是常见的运算符,可以加深对String的认识和运算符重载的理解。
• 两个常用的函数,包括取字符串长度和取C类型的字符串。
• 两个处理输入输出的运算符重载,为了使用的方便,这里把这两个运算符定义为友元函数。
整体的类的框架如下所示。

class String
{
public:
    String(const char *str = NULL); //通用构造函数
    String(const String &str);      //拷贝构造函数
    ~String();                     //析构函数

    String operator+(const String &str) const;  //重载+
    String& operator=(const String &str);       //重载=
    String& operator+=(const String &str);      //重载+=
    bool operator==(const String &str) const;   //重载==
    char& operator[](int n) const;              //重载[]

    size_t size() const;        //获取长度
    const char* c_str() const;  //获取C字符串

    friend istream& operator>>(istream &is, String &str);//输入
    friend ostream& operator<<(ostream &os, String &str);//输出

private:
    char *data;     //字符串
    size_t length;  //长度
};

注意,类的成员函数中,有一些是加了const修饰的,表示这个函数不会对类的成员进行任何修改。一些函数的输入参数也加了const修饰,表示该函数不会对改变这个参数的值。
下面逐个进行成员函数的实现。
同样构造函数适用一个字符串数组进行String的初始化,默认的字符串数组为空。这里的函数定义中不需要再定义参数的默认值,因为在类中已经声明过了。
另外,适用C函数strlen的时候需要注意字符串参数是否为空,对空指针调用strlen会引发内存错误。

String::String(const char *str)//通用构造函数
{
    if (!str)
    {
        length = 0;
        data = new char[1];
        *data = '\0';
    }
    else
    {
        length = strlen(str);
        data = new char[length + 1];
        strcpy(data, str);
    }
}
拷贝构造函数需要进行深复制。
String::String(const String &str)//拷贝构造函数
{
    length = str.size();
    data = new char[length + 1];
    strcpy(data, str.c_str());
}
析构函数需要进行内存的释放及长度的归零。
String::~String()//析构函数
{
    delete []data;
    length = 0;
}
重载字符串连接运算,这个运算会返回一个新的字符串。
String String::operator+(const String &str) const//重载+
{
    String newString;
    newString.length = length + str.size();
    newString.data = new char[newString.length + 1];
    strcpy(newString.data, data);
    strcat(newString.data, str.data);
    return newString;
}

重载字符串赋值运算,这个运算会改变原有字符串的值,为了避免内存泄露,这里释放了原先申请的内存再重新申请一块适当大小的内存存放新的字符串。

String& String::operator=(const String &str)//重载=
{
    if (this == &str)   return *this;

    delete []data;
    length = str.length;
    data = new char[length + 1];
    strcpy(data, str.c_str());
    return *this;
}

重载字符串+=操作,总体上是以上两个操作的结合。

String& String::operator+=(const String &str)//重载+=
{
    length += str.length;
    char *newData = new char[length + 1];
    strcpy(newData, data);
    strcat(newData, str.data);
    delete []data;
    data = newData;
    return *this;
}

重载相等关系运算,这里定义为内联函数加快运行速度。

inline bool String::operator==(const String &str) const//重载==
{
    if (length != str.length)   return false;
    return strcmp(data, str.data) ? false : true;
}

重载字符串索引运算符,进行了一个简单的错误处理,当长度太大时自动读取最后一个字符。

inline char& String::operator[](int n) const//重载[]
{
    if (n >= length) return data[length-1]; //错误处理
    else return data[n];
}

重载两个读取私有成员的函数,分别读取长度和C字符串。

inline size_t String::size() const//获取长度
{
    return length;
}

重载输入运算符,先申请一块足够大的内存用来存放输入字符串,再进行新字符串的生成。这是一个比较简单朴素的实现,网上很多直接is>>str.data的方法是错误的,因为不能确定str.data的大小和即将输入的字符串的大小关系。

istream& operator>>(istream &is, String &str)//输入
{
    char tem[1000];  //简单的申请一块内存
    is >> tem;
    str.length = strlen(tem);
    str.data = new char[str.length + 1];
    strcpy(str.data, tem);
    return is;
}

重载输出运算符,只需简单地输出字符串的内容即可。注意为了实现形如cout<

ostream& operator<<(ostream &os, String &str)//输出
{
    os << str.data;
    return os;
}
inline const char* String::c_str() const//获取C字符串
{
    return data;
}

13. 进程和线程的区别
进程:是并发执行的程序在执行过程中分配和管理资源的基本单位,是一个动态概念,竞争计算机系统资源的基本单位。
线程:是进程的一个执行单元,是进程内科调度实体。比进程更小的独立运行的基本单位。线程也被称为轻量级进程。
一个程序至少一个进程,一个进程至少一个线程。
为什么会有线程?
  每个进程都有自己的地址空间,即进程空间,在网络或多用户换机下,一个服务器通常需要接收大量不确定数量用户的并发请求,为每一个请求都创建一个进程显然行不通(系统开销大响应用户请求效率低),因此操作系统中线程概念被引进。
• 线程的执行过程是线性的,尽管中间会发生中断或者暂停,但是进程所拥有的资源只为改线状执行过程服务,一旦发生线程切换,这些资源需要被保护起来。
• 进程分为单线程进程和多线程进程,单线程进程宏观来看也是线性执行过程,微观上只有单一的执行过程。多线程进程宏观是线性的,微观上多个执行操作。
线程的改变只代表CPU的执行过程的改变,而没有发生进程所拥有的资源的变化。 
进程线程的区别:
• 地址空间:同一进程的线程共享本进程的地址空间,而进程之间则是独立的地址空间。
• 资源拥有:同一进程内的线程共享本进程的资源如内存、I/O、cpu等,但是进程之间的资源是独立的。
     一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。
     进程切换时,消耗的资源大,效率高。所以涉及到频繁的切换时,使用线程要好于进程。同样如果要求同时进行并且又要共享某些变量的并发操作,只能用线程不能用进程
• 执行过程:每个独立的进程程有一个程序运行的入口、顺序执行序列和程序入口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
• 线程是处理器调度的基本单位,但是进程不是。
• 两者均可并发执行。
优缺点:
  线程执行开销小,但是不利于资源的管理和保护。线程适合在SMP机器(双CPU系统)上运行。
  进程执行开销大,但是能够很好的进行资源管理和保护。进程可以跨机器前移。
何时使用多进程,何时使用多线程?
对资源的管理和保护要求高,不限制开销和效率时,使用多进程。
要求效率高,频繁切换时,资源的保护管理要求不是很高时,使用多线程。

14. Static_cast与强制类型转换的区别
static_cast在编译时会进行类型检查,而强制转换不会。
腾讯
1. 实现一个算法来删除单链表中间的一个结点,只给出指向那个结点的指针。
例子:
输入:指向链表a->b->c->d->e中结点c的指针
结果:不需要返回什么,得到一个新链表:a->b->d->e

这个问题的关键是你只有一个指向要删除结点的指针,如果直接删除它,这条链表就断了。 但你又没办法得到该结点之前结点的指针,是的,它连头结点也不提供。在这种情况下, 你只能另觅他径。重新审视一下这个问题,我们只能获得从c结点开始后的指针, 如果让你删除c结点后的某个结点,那肯定是没问题的。比如删除结点d,那只需要把c 的next指针指向e,然后delete d就ok了。好的,如果我们就删除结点d,我们将得到 a->b->c->e,和目标链表只差了一个结点。怎么办呢?把d的数据给c! 结点结构都是一样的,删除谁都一样,最关键的是结点中的数据,只要我们留下的是数据 a->b->d->e就OK了。
思路已经有了,直接写代码?等等,写代码之前,让我们再简单分析一 下可能出现的各种情况(当然包括边界情况)。如果c指向链表的:1.头结点;2.中间结点。 3.尾结点。4.为空。情况1,2属于正常情况,直接将d的数据给c,c的next指针指向d 的next指向所指结点,删除d就OK。情况4为空,直接返回。情况3比较特殊,如果c 指向尾结点,一般会认为直接删除c就ok了,反正c后面也没有结点了,不用担心链表断开。 可是真的是这样吗?代码告诉我们,直接删除c,指向c的那个结点(比如说b)的next指针 并不会为空。也就是说,如果你打印这个链表,它还是会打印出和原来链表长度一样的链表, 而且最后一个元素为0!
关于这一点,原书CTCI中是这么讲的,这正是面试官希望你指出来的。然后, 你可以做一些特殊处理,比如当c是尾结点时,将它的数据设置为一些特殊字符, 这样在打印时就可以不打印它。当然也可以直接不处理这种情况,原书里的代码就是这么做 的。这里,也直接不处理这种情况。代码如下

bool remove(node* c)
{
        If(c==NULL || c->next=NULL) return false;
        Node* q =c->next;
        c->next=q->next;
        c->data=q->data;
        delete q;
        return true;
}

2. 设计一个日历系统,要通用高效。
3. 求一个数组的和最大的连续子数组。
使用thisSum来计算当前连续子数组的和,如果thisSum小于0,那么无论后面再加上什么数字都只会让子数组变小,所以抛弃当前子数组,重新开始计算子数组的值。
可以看到这个算法的时间复杂度为O(n)而且控件复杂度为S(n),是解决这一个问题非常有效的一个算法。

for (int i = 0; i < array_length;++i)
    {
        thissum += a[i];
        if (thissum>sum)
        {
            if (height == 0)
            {
                low = i;
            }
            sum = thissum;
            height=i-low+1;
        }
        else if (thissum < 0)
        {
            thissum = 0;
        }

    }

4. 树 这一数据结构的特点
树形结构是一类非线性结构,树形结构是节点之间有分支,并且具有层次关系的结构,类似于自然界中的树;

1. 声明和定义的区别;
对于变量来说,生明就是定义;
void sum(int a,int b);这是函数的声明
void sum(int a,int b)
{
}
整体是函数的定义 ,函数的定义没有分号 而且要加上一对花括号 ,里边是函数的实现。
函数一定要在定义前声明否则会报错 。我一般在主函数前写上函数的声明 ,然后在主函数之后写函数的定义。

2. 命名空间的作用,在一个大的项目工程中使用命名空间应该注意什么;
协同开发:因为同名的命名空间可以融合,所以当多人开发时,可以将自己的内容
都裹上同一个名字的命名空间
具体操作是 例如你开发一个类 将它的头文件、和cpp文件中都用命名空
间包装好,当多个文件合一起后 include相应头文件 在解开统一的命名
空间,就可以使用里面的内容
3. 模板库STL有哪些基础容器,这些容器的比较;
Vector可以认为是1个或N个更多元素的数组;list是由节点组成的双向链表,每个节点包含一个元素;deque是又包含N个连续的指向不同元素的指针组成的数组;set是有节点组成的,每个节点包含一个元素,节点之间以某种谓词排序;multiset是允许存在连个次序相等的元素的集合;map是由{键:值}对组成的集合,同样以某种谓词排序,谓词和数据对有某种作用;multimap是允许键对包含相等次序的映射;
Vector,deque,hashtable,
queue,stack,list,set,map,hash_map,bitset,priority_queue
1.vector
vector是一种动态数组,在内存中具有连续的存储空间,支持快速随机访问。由于具有连续的存储空间,所以在插入和删除操作方面,效率比较慢。vector有多个构造函数,默认的构造函数是构造一个初始长度为0的内存空间,且分配的内存空间是以2的倍数动态增长的,在push_back的过程中,若发现分配的内存空间不足,则重新分配一段连续的内存空间,其大小是现在连续空间的2倍,再将原先空间中的元素复制到新的空间中,性能消耗比较大,尤其是当元素是非内部数据时。vector的另一个常见的问题就是clear操作。clear函数只是把vector的size清为零,但vector中的元素在内存中并没有消除,所以在使用vector的过程中会发现内存消耗会越来越多,导致内存泄露,现在经常用的方法是swap函数来进行解决:利用swap函数,和临时对象交换,交换以后,临时对象消失,释放内存。
如果你需要高效的随即存取,而不在乎插入和删除的效率,使用vector。
2.list
list是一个双向链表,因此它的内存空间是可以不连续的,通过指针来进行数据的访问,这使list的随机存储变得非常低效,因此list没有提供[]操作符的重载。但list可以很好地支持任意地方的插入和删除,只需移动相应的指针即可。
如果你需要大量的插入和删除,而不关心随即存取,则应使用list。
3.deque
deque和vector类似,支持快速随机访问。二者最大的区别在于,vector只能在末端插入数据,而deque支持双端插入数据。deque的内存空间分布是小片的连续,小片间用链表相连,实际上内部有一个map的指针。deque空间的重新分配要比vector快,重新分配空间后,原有的元素是不需要拷贝的,可以认为deque是vector和list的折中。
如果你需要随即存取,而且关心两端数据的插入和删除,则应使用deque。
4.map
map是一种关联容器,该容器用唯一的关键字来映射相应的值,即具有key-value功能。map内部自建一棵红黑树(一种自平衡二叉树),这棵树具有数据自动排序的功能,所以在map内部所有的数据都是有序的,以二叉树的形式进行组织。map的插入和删除效率比其他序列的容器高,因为对关联容器来说,不需要做内存的拷贝和移动,只是指针的移动。由于map的每个数据对应红黑树上的一个节点,这个节点在不保存你的数据时,是占用16个字节的,一个父节点指针,左右孩子指针,还有一个枚举值(标示红黑色),所以map的其中的一个缺点就是比较占用内存空间。
默认情况下内部数据进行升序排列。
multimap是map的单key对多value的容器。
5.hash_map
hash_map使用hash表来排列配对,hash表是使用关键字来计算表位置。当这个表的大小合适,并且计算算法合适的情况下,hash表的算法复杂度为O(1)的,但是这是理想的情况下的,如果hash表的关键字计算与表位置存在冲突,那么最坏的复杂度为O(n)。
选用map还是hash_map,关键是看关键字查询操作次数,以及你所需要保证的是查询总体时间还是单个查询的时间。如果是要很多次操作,要求其整体效率,那么使用hash_map,平均处理时间短。如果是少数次的操作,使用 hash_map可能造成不确定的O(N),那么使用平均处理时间相对较慢、单次处理时间恒定的map,便更好些。
默认情况下内部数据不排序。
6.set
set也是一种关联性容器,它同map一样,底层使用红黑树实现,插入删除操作时仅仅移动指针即可,不涉及内存的移动和拷贝,所以效率比较高。set中的元素都是唯一的,而且默认情况下会对元素进行升序排列。所以在set中,不能直接改变元素值,因为那样会打乱原本正确的顺序,要改变元素值必须先删除旧元素,再插入新元素。不提供直接存取元素的任何操作函数,只能通过迭代器进行间接存取。
multiset类似于数学里面的集合,集合中可以包含重复的元素。
7.queue
queue是一个队列,实现先进先出功能,queue不是标准的STL容器,却以标准的STL容器为基础。queue是在deque的基础上封装的。之所以选择deque而不选择vector是因为deque在删除元素的时候释放空间,同时在重新申请空间的时候无需拷贝所有元素。
8.stack
stack是实现先进后出的功能,和queue一样,也是内部封装了deque。自己不直接维护被控序列的模板类,而是它存储的容器对象来为它实现所有的功能。

4. 什么是全局变量、局部变量、静态变量、成员变量、静态成员变量,各自的生命周期;
1)全局变量:
作用域:全局作用域(只需要在一个源文件中定义,就可以作用于所有的源文件);
生命周期:程序运行期一直存在;
引用方法:其他文件如果要使用,必须用extern 关键字声明要引用的全局变量;
内存分布:全局(静态存储区)。
注意:如果再两个文件中都定义了相同名字的全局变量,则连接错误:变量重定义。
2)全局静态变量:
生命周期:程序运行期一直存在;
作用域:文件作用域(只在被定义的文件中可见:static的一个作用就是隐藏)
内存分布:全局(静态存储区)。
定义方法:static关键字,const关键字(注意C/C++意义不同)
注意:只要文件不相互包含,两个不同的文件中是可以定义完全相同的两个全局静态变量的。
3)静态局部变量:
生命周期:程序运行期一直存在;(超过其作用域便无法被引用)
作用域:局部作用域(只在局部作用于可见)
内存分布:全局(静态存储区)。
定义方法:局部作用域中用static定义。
注意:只被初始化一次,多线程中需要加锁保护。
4)局部变量:
生命周期:程序运行处局部作用域 即被销毁。
作用域:局部作用域(只在局部作用于可见)
内存分布:栈区

5. 用代码简单实现 重载、继承、覆盖;
重载是指不同的函数使用相同的函数名,但是函数的参数个数或类型不同。调用的时候根据函数的参数来区别不同的函数。
覆盖(也叫重写)是指在派生类中重新对基类中的虚函数(注意是虚函数)重新实现。即函数名和参数都一样,只是函数的实现体不一样。

6. delete 与delete[]的区别
c++中对new申请的内存的释放方式有delete和delete[两种方式,到底这两者有什么区别呢?

1.我们通常从教科书上看到这样的说明:
delete 释放new分配的单个对象指针指向的内存
delete[] 释放new分配的对象数组指针指向的内存
那么,按照教科书的理解,我们看下下面的代码:
int *a = new int[10];
delete a; //方式1
delete [] a; //方式2
肯定会有很多人说方式1肯定存在内存泄漏,是这样吗?
(1). 针对简单类型 使用new分配后的不管是数组还是非数组形式内存空间用两种方式均可 如:
int *a = new int[10];
delete a;
delete [] a;
此种情况中的释放效果相同,原因在于:分配简单类型内存时,内存大小已经确定,系统可以记忆并且进行管理,在析构时,系统并不会调用析构函数,
它直接通过指针可以获取实际分配的内存空间,哪怕是一个数组内存空间(在分配过程中 系统会记录分配内存的大小等信息,此信息保存在结构体_CrtMemBlockHeader中,
具体情况可参看VC安装目录下CRT\SRC\DBGDEL.cpp)
(2). 针对类Class,两种方式体现出具体差异
当你通过下列方式分配一个类对象数组:

   class A
   {
   private:
      char *m_cBuffer;
      int m_nLen;
   public:
      A(){ m_cBuffer = new char[m_nLen]; }
      ~A() { delete [] m_cBuffer; }
   };
   A *a = new A[10];
   delete a;         //仅释放了a指针指向的全部内存空间 但是只调用了a[0]对象的析构函数 剩下的从a[1]到a[9]这9个用户自行分配的m_cBuffer对应内存空间将不能释放 从而造成内存泄漏
   delete [] a;      //调用使用类对象的析构函数释放用户自己分配内存空间并且   释放了a指针指向的全部内存空间

小结:
如果ptr代表一个用new申请的内存返回的内存空间地址,即所谓的指针,那么:
delete ptr 代表用来释放内存,且只用来释放ptr指向的内存。
delete[] rg 用来释放rg指向的内存,!!还逐一调用数组中每个对象的destructor!!
对于像int/char/long/int*/struct等等简单数据类型,由于对象没有destructor,所以用delete 和delete [] 是一样的!但是如果是C++对象数组就不同了!
7. Point( int x, int y, string name ){ _x = 0; _y = 0; _name = name; }
_name = name 这个表达式会调用string类的缺省构造函数*一次,再调用Operator=函数进行赋值一次。所以需调用两次函数:一次构造,一次赋值
用初始化列表进行初始化

Point( int x, int y, string name ):_x(x),_y(y), _name(name){} 

_name会通过拷贝构造函数仅以一个函数调用的代码完成初始化
即使是一个很简单的string类型,不必要的函数调用也会造成很高的代价。随着类越来越大,越来越复杂,它们的构造函数也越来越大而复杂,那么对象创建的代价也越来越高,所以一般情况下使用初始化列表进行初始化,不但可以满足const和引用成员的初始化要求,还可以避免低效的初始化数据成员。
*缺省构造函数是C++及其他一些面向对象程序设计语言中,对象的不需要参数即可调用的构造函数。对象生成时如果没有显式地调用构造函数,则缺省构造函数会被自动调用。C++标准规定,如果构造函数没有参数(nullary),或者构造函数的所有参数都有缺省值(default value),都算作缺省构造函数。[1]一个类只能有一个缺省构造函数
成员类型是没有默认构造函数的类。若没有提供显示初始化式,则编译器隐式使用成员类型的默认构造函数,若类没有默认构造函数,则编译器尝试使用默认构造函数将会失败。
note:
初始化列表的成员初始化顺序:
C++初始化类成员时,是按照声明的顺序初始化的,而不是按照出现在初始化列表中的顺序
为什么构造函数不能声明为虚函数,析构函数可以
构造函数不能声明为虚函数,析构函数可以声明为虚函数,而且有时是必须声明为虚函数。
不建议在构造函数和析构函数里面调用虚函数。

构造函数不能声明为虚函数的原因是:
1 构造一个对象的时候,必须知道对象的实际类型,而虚函数行为是在运行期间确定实际类型的。而在构造一个对象时,由于对象还未构造成功。编译器无法知道对象 的实际类型,是该类本身,还是该类的一个派生类,或是更深层次的派生类。无法确定。。。

2 虚函数的执行依赖于虚函数表。而虚函数表在构造函数中进行初始化工作,即初始化vptr,让他指向正确的虚函数表。而在构造对象期间,虚函数表还没有被初 始化,将无法进行。

虚函数的意思就是开启动态绑定,程序会根据对象的动态类型来选择要调用的方法。然而在构造函数运行的时候,这个对象的动态类型还不完整,没有办法确定它到底是什么类型,故构造函数不能动态绑定。(动态绑定是根据对象的动态类型而不是函数名,在调用构造函数之前,这个对象根本就不存在,它怎么动态绑定?)
编译器在调用基类的构造函数的时候并不知道你要构造的是一个基类的对象还是一个派生类的对象。

析构函数设为虚函数的作用:
解释:在类的继承中,如果有基类指针指向派生类,那么用基类指针delete时,如果不定义成虚函数,派生类中派生的那部分无法析构。
例:

#include "stdafx.h"
#include "stdio.h"
class A
{
public:
A();
virtual~A();
};
A::A()
{
}

A::~A()
{
printf("Delete class APn");
}
class B : public A
{
public:
B();
~B();
};

B::B()
{ }

B::~B()
{
printf("Delete class BPn");
}
int main(int argc, char* argv[])
{
A *b=new B;
delete b;
return 0;
}

输出结果为:Delete class B
Delete class A

如果把A的virtual去掉:那就变成了Delete class A也就是说不会删除派生类里的剩余部分内容,也即不调用派生类的虚函数

因此在类的继承体系中,基类的析构函数不声明为虚函数容易造成内存泄漏。所以如果你设计一定类可能是基类的话,必须要声明其为虚函数。正如Symbian中的CBase一样。

Note:
1. 如果我们定义了一个构造函数,编译器就不会再为我们生成默认构造函数了。
2. 编译器生成的析构函数是非虚的,除非是一个子类,其父类有个虚析构,此时的函数虚特性来自父类。
3. 有虚函数的类,几乎可以确定要有个虚析构函数。
4. 如果一个类不可能是基类就不要申明析构函数为虚函数,虚函数是要耗费空间的。
5. 析构函数的异常退出会导致析构不完全,从而有内存泄露。最好是提供一个管理类,在管理类中提供一个方法来析构,调用者再根据这个方法的结果决定下一步的操作。
6. 在构造函数不要调用虚函数。在基类构造的时候,虚函数是非虚,不会走到派生类中,既是采用的静态绑定。显然的是:当我们构造一个子类的对象时,先调用基类的构造函数,构造子类中基类部分,子类还没有构造,还没有初始化,如果在基类的构造中调用虚函数,如果可以的话就是调用一个还没有被初始化的对象,那是很危险的,所以C++中是不可以在构造父类对象部分的时候调用子类的虚函数实现。但是不是说你不可以那么写程序,你这么写,编译器也不会报错。只是你如果这么写的话编译器不会给你调用子类的实现,而是还是调用基类的实现。
7.在析构函数中也不要调用虚函数。在析构的时候会首先调用子类的析构函数,析构掉对象中的子类部分,然后在调用基类的析构函数析构基类部分,如果在基类的析构函数里面调用虚函数,会导致其调用已经析构了的子类对象里面的函数,这是非常危险的。

  1. 记得在写派生类的拷贝函数时,调用基类的拷贝函数拷贝基类的部分,不能忘记了。
    内存字节对齐
    1:数据成员对齐规则:结构(struct)(或联合(union))的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员存储的起始位置要从该成员大小或者成员的子成员大小(只要该成员有子成员,比如说是数组,结构体等)的整数倍开始(比如int在32位机为4字节,则要从4的整数倍地址开始存储。

    2:结构体作为成员:如果一个结构里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储.(struct a里存有struct b,b里有char,int ,double等元素,那b应该从8的整数倍开始存储.)

3:收尾工作:结构体的总大小,也就是sizeof的结果,.必须是其内部最大成员的整数倍.不足的要补齐.

typedef struct bb
{
 int id;             //[0]....[3]
 double weight;      //[8].....[15]      原则1
 float height;      //[16]..[19],总长要为8的整数倍,补齐[20]...[23]     原则3
}BB;

typedef struct aa
{
 char name[2];     //[0],[1]
 int  id;         //[4]...[7]          原则1

 double score;     //[8]....[15]    
 short grade;    //[16],[17]        
 BB b;             //[24]......[47]          原则2
}AA;

int main()
{
  AA a;
  cout<<sizeof(a)<<" "<<sizeof(BB)<<endl;
  return 0;
}

结果是

48 24
ok,上面的全看明白了,内存对齐基本过关.

再讲讲#pragma pack().

在代码前加一句#pragma pack(1),你会很高兴的发现,上面的代码输出为

32 16
bb是4+8+4=16,aa是2+4+8+2+16=32;

这不是理想中的没有内存对齐的世界吗.没错,#pragma pack(1),告诉编译器,所有的对齐都按照1的整数倍对齐,换句话说就是没有对齐规则.

ps:Vc,Vs等编译器默认是#pragma pack(8),所以测试我们的规则会正常;注意gcc默认是#pragma pack(4),并且gcc只支持1,2,4对齐。套用三原则里计算的对齐值是不能大于#pragma pack指定的n值。
DLL分配的内存如何在EXE里面释放
1. 保证内存分配和清除的统一性:如果一个DLL提供一个能够分配内存的函数,那么这个DLL同时应该提供一个函数释放这些内存。数据的创建和清除应该在同一个层次上。

曾经遇到过这样的例子:在dll中分配了一块内存,通过PostMessage将其地址传给应用。然后应用去释放它,结果总是报异常。

2.如果exe用 MFC Appwizard方式生成, dll用win32方式生成,则运行时会出现错误。进一步用单步跟踪,发现mfc方式和win32方式下的new操作符是用不同方式实现的,源程序分别在VC目录的文件 Afxmem.cpp和new.cpp中。有兴趣的话可以自已跟踪一下。

因为dll输出函数后,并不知道是哪一个模拟调用它,因此new和delete配对时最好在一个文件中,这样可以保证一致性。

  1. 问题主要在于DLL和EXE主程序中分配内存的堆不一样,你可以不用new和delete,而是用

1) ::HeapAlloc(::GetProcessHeap(),…)和::HeapFree(::GetProcessHeap(),…)

2) ::GlobalAlloc()和::GlobalFree()

这两对API,这样无论在DLL中还是在主程序中都是在进程默认堆中分配,就不会出错了。

  1. 还有一个办法,就是把dll的Settings的C/C++选项卡的Code Generation的Use Run-time liberary改成Debug Multithreaded DLL,在Release版本中改成Multithreaded DLL,就可以直接使用new和delete了。不过MFC就不能用Shared模式了。
    C++多线程
    C++11 新标准中引入了四个头文件来支持多线程编程,他们分别是 ,,,和。

:该头文主要声明了两个类, std::atomic 和 std::atomic_flag,另外还声明了一套 C 风格的原子类型和与 C 兼容的原子操作的函数。
:该头文件主要声明了 std::thread 类,另外 std::this_thread 命名空间也在该头文件中。
:该头文件主要声明了与互斥量(mutex)相关的类,包括 std::mutex 系列类,std::lock_guard, std::unique_lock, 以及其他的类型和函数。
:该头文件主要声明了与条件变量相关的类,包括 std::condition_variable 和 std::condition_variable_any。
:该头文件主要声明了 std::promise, std::package_task 两个 Provider 类,以及 std::future 和 std::shared_future 两个 Future 类,另外还有一些与之相关的类型和函数,std::async() 函数就声明在此头文件中。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值