C++学习笔记

C++学习笔记

语言类

总结一些容易忘记的小知识吧,本文持续更新。。。

关键字总结

volatile

C/C++ Volatile关键词深度剖析

Volatile关键词的第一个特性:易变形。所谓的异变性,在汇编阶段反映出来,就是两条语句,下一条语句对应 volatile 变量的寄存器内容,而是重新从内存中读取

Volatile关键词第二个特性:“不可优化性”。volatile 告诉编译器,不要对我这个变量进行各种激进的优化,甚至将变量直接消除,保证程序员写在代码中的指令,一定会被执行。

Volatile关键词的第三个特性:“顺序性”。 能够保证 volatile 变量间的顺序性,编译器不会进行乱序优化

C/C++ Volatile变量,与非 Volatile 变量之间操作,是可能被编译器交换顺序的。C/C++ Vloatile 变量间的操作,是不会被编译器交换顺序的。哪怕将所有的变量全部声明为 volatile, 哪怕杜绝了编译器的乱序优化,但是针对生成的汇编代码,CPU有可能仍旧会乱序执行指令,导致程序依赖的逻辑出错,volatile对此无能为力。针对这个多线程的应用,真正正确的做法,是构建一个happens-before语义。

static

控制变量的存储方式和可见性。 

1. 修饰局部变量
一般情况下,对于局部变量是存放在栈区的,并且局部变量的生命周期在该语句块执行结束时便结束了。但是如果使用 static 进行修饰的话,该变量便存放在静态数据区,其生命周期一直持续到整个程序执行结束。但是,虽然 static 对局部变量进行修饰后,其生命周期以及存储空间发生了变化,但其作用域并没有改变,仍然是一个局部变量,作用域仅限于该语句块。

2. 修饰全局变量
对于一个全局变量,它既可以在本源文件中访问到,也可以在同工程的其它源文件中被访问(只需要 extern 进行声明即可)。用 static 对全局变量进行修饰,改变了其作用域的范围,由原来的整个工程可见变仅本源可见。

3. 修饰函数
用 static 修饰函数的话,情况与修饰全局变量大同小异,改变了函数的作用域。

4. C++ 中的 static
如果在C++中对类中的某个函数用 static 进行修饰,则便是该函数属于一个类,而不是属于此类的任何特定对象;如果对类中某个变量进行 static 修饰,表示该变量为类以及所有类对象所有。他们的存储空间中都只存在一个副本。可以通过类和对象去调用。

const

 const 名叫常量限定符,用来限定特定变量,以通知编译器该变量是不可修改的。习惯性的使用 const,可以避免在函数中对某些不应修改的变量造成可能的改动。

1. const 修饰基本数据类型

(1) const 修饰基本数据类型

基本数据类型,修饰符 const 可以用在类型说明符前,也可以用在类型说明符后,其结果是一样的。在使用这些常量的时候,这些常量的值无法被改变。

(2) const 修饰指针变量*及引用变量&

如果 const 位于 * 的左侧,则 const 就是用来修饰指针所指向的变量,称为常量指针。const int *p;  int const *p; 二者皆可,此时指针所指向的内存数据不能被修改,但是本⾝身可以修改。

如果 const 位于 * 的右侧,则const就是修饰指针本身,称为常量指针。int * const p; 此时指针指针变量不能被修改,但是它所指向内存空间可以被修改。

2. const 应用到函数中

(1) 作为参数的 const 修饰符

调用函数的时候,用相应的变量初始化 const 常量,则在函数体中,按照 const 所修饰的部分进行常量化,保护了原对象的属性无法在函数中更改。
[注意]:参数const通常用于参数为指针或引用的情况;

(2) 作为函数返回值的 const 修饰符
声明了返回值后, const 按照“修饰原则”进行修饰,起到相应的保护作用,不能被作为左值。

3. const在类中的用法
不能在类声明中初始化 const 数据成员。正确的使用 const 实现方法为:const 数据成员的初始化只能在类的构造函数的初始化表中进行,与初始化基类成员一样。

4. const修饰类对象,定义常量对象 
常量对象只能调用常量函数,别的成员函数都不能调用。

extern

在C语言中,修饰符 extern 用在变量或者函数的声明前,用来说明“此变量/函数是在别处定义的,要在此处引用”。

注意 extern 声明的位置对其作用域也有关系,如果是在 main 函数中进行声明的,则只能在 main 函数中调用,在其它函数中不能调用。其实要调用其它文件中的函数和变量,只需把该文件用 #include 包含进来即可,为啥要用 extern?因为用 extern 会加速程序的编译过程,这样能节省时间。

在 C++ 中 extern 还有另外一种作用,用于指示 C 或者 C++ 函数的调用规范。比如在 C++ 中调用 C 库函数,就需要在 C++ 程序中用 extern “C” 声明要引用的函数。这是给链接器用的,告诉链接器在链接的时候用C函数规范来链接。主要原因是 C++ 和 C 程序编译完成后在目标代码中命名规则不同,用此来解决名字匹配的问题。

宏定义和展开、内联函数区别

内联函数是代码被插入到调用者代码处的函数。如同 #define 宏,内联函数通过避免被调用的开销来提高执行效率,尤其是它能够通过调用(“过程化集成”)被编译器优化。 宏定义不检查函数参数,返回值什么的,只是展开,相对来说,内联函数会检查参数类型,所以更安全。	内联函数和宏很类似,而区别在于,宏是由预处理器对宏进行替代,而内联函数是通过编译器控制来实现的。而且内联函数是真正的函数,只是在需要用到的时候,内联函数像宏一样的展开,所以取消了函数的参数压栈,减少了调用的开销。

宏是预编译器的输入,然后宏展开之后的结果会送去编译器做语法分析。宏与函数等处于不同的级别,操作不同的实体。宏操作的是 token, 可以进行 token 的替换和连接等操作,在语法分析之前起作用。而函数是语言中的概念,会在语法树中创建对应的实体,内联只是函数的一个属性。

对于问题:有了函数要它们何用?答案是:一:函数并不能完全替代宏,有些宏可以在当前作用域生成一些变量,函数做不到。二:内联函数只是函数的一种,内联是给编译器的提示,告诉它最好把这个函数在被调用处展开,省掉一个函数调用的开销(压栈,跳转,返回)

内联函数也有一定的局限性。就是函数中的执行代码不能太多了,如果,内联函数的函数体过大,一般的编译器会放弃内联方式,而采用普通的方式调用函数。这样,内联函数就和普通函数执行效率一样

内联函数必须是和函数体申明在一起,才有效。

常见的库函数实现

memcpy

//内存拷贝函数,memcpy 函数的功能是从源 src 所指的内存地址的起始位置开始拷贝 n 个字节到目标 dest 所指的内存地址的起始位置中
void* memcpy(void* dest,void* src,size_t n){
    assert(dest != NULL && src != NULL); //assert 的作用是现计算表达式 expression ,如果其值为假(即为0),那么它先向 stderr 打印一条出错信息,然后通过调用 abort 来终止程序运行。
    if(src < dest && (char*)src + n > dest){
        char* pdst = (char*)dest+n-1;
        char* psrc = (char*)src+n-1;
        while(n--){
            *pdst-- = *psrc--;
        }
    }else{
        char* pdest = (char*)dest;
        char* psrc = (char*)src;
        while(n--){
            *pdest++ = *psrc++;
        }
    }
    return dest;
}

memset

// memset()的函数, 它可以一字节一字节地把整个数组设置为一个指定的值。注意,按照字节赋值
void* memset(void* src,int c,size_t n){
    assert(src != NULL);
    char* psrc = (char*)src;
    while(n--){
        *psrc++ = (char)c;
    }
    return src;
}

strcpy

//把 src 所指向的字符串复制到 dest。需要注意的是如果目标数组 dest 不够大,而源字符串的长度又太长,可能会造成缓冲溢出的情况。
char* strcpy(char* dest,const char* src){
    assert(dest != NULL && src != NULL);

    size_t n = strlen(src);
    if(src < dest && src + n > dest){
        char* pdst = dest + n;
        *pdst-- = '\0';
        src = src + n - 1;
        while(n--){
            *pdst-- = *src--;
        }
    }else{
        char* pdst = dest;
        while((*pdst++ = *src++) != 0);
    }
    return dest;
}

strcat

//把 src 所指向的字符串追加到 dest 所指向的字符串的结尾。
char* strcat(char* dest,const char* src){
    assert(dest != NULL && src != NULL);
    char* pdst = dest;
    while(*pdst != '\0'){
        pdst++;
    }
    while((*pdst++ = *src++) != '\0');
    return dest;
}

strcmp

//把 str1 所指向的字符串和 str2 所指向的字符串进行比较。
int strcmp(const char* s1,const char* s2){
    assert(s1 != NULL && s2 != NULL);
    while(s1 && s2 && *s1 == *s2){
        s1++;
        s2++;
    }
    return *s1 - *s2;
}

高危库函数

字符串处理函数

strcpy() 函数将源字符串复制到缓冲区。没有指定要复制字符的具体数目!如果源字符串碰巧来自用户输入,且没有专门限制其大小,则有可能会造成缓冲区溢出!
我们也可以使用strncpy来完成同样的目的:
strncpy (dst, src, dst_size-1);
如果 src 比 dst 大,则该函数不会抛出一个错误;当达到最大尺寸时,它只是停止复制字符。注意上面调用 strncpy() 中的 -1。如果 src 比 dst 长,则那给我们留有空间,将一个空字符放在 dst 数组的末尾。
但是! strncpy()也不完全安全,也有可能把事情搞糟。即使“安全”的调用有时会留下未终止的字符串,或者会发生微妙的相差一位错误。
确保 strcpy() 不会溢出的另一种方式是,在需要它时就分配空间,确保通过在源字符串上调用 strlen() 来分配足够的空间。
strcat() 函数非常类似于 strcpy(),除了它可以将一个字符串合并到缓冲区末尾。它也有一个类似的、更安全的替代方法 strncat()。如果可能,使用 strncat() 而不要使用 strcat()。
函数 sprintf() 和 vsprintf() 是用来格式化文本和将其存入缓冲区的通用函数。它们可以用直接的方式模仿 strcpy() 行为。换句话说,使用 sprintf() 和 vsprintf() 与使用 strcpy() 一样,都很容易对程序造成缓冲区溢出。

sprintf() 的许多版本带有使用这种函数的更安全的方法。可以指定格式字符串本身每个自变量的精度。sprintf 采用” * ”来占用一个本来需要一个指定宽度或精度的常数数字的位置,而实际的宽度或精度就可以和其它被打印的变量一样被提供出来。

字符读取函数

永远不要使用 gets()

该函数从标准输入读入用户输入的一行文本,它在遇到 EOF 字符或换行字符之前,不会停止读入文本。也就是:gets() 根本不执行边界检查。因此,使用 gets() 总是有可能使任何缓冲区溢出。

作为一个替代方法,可以使用方法 fgets()。它可以做与 gets() 所做的同样的事情,但它接受用来限制读入字符数目的大小参数,因此,提供了一种防止缓冲区溢出的方法。
getchar()、fgetc()、getc()、read()

STL原理及实现

STL提供六大组件,彼此组合套用

STL六大组件介绍

1. 容器(Containers):各种数据结构,如:序列式容器 vector、list、deque;关联式容器 set、map、multist、multimap。用来存放各种数据,从实现角度来看,STL容器是一种 class template。

2. 算法(algorithms):各种常用算法,如:sort、search、copy、erase。从实现的角度来看,STL算法是一种 function template。注意一个问题:任何的一个STL算法,都需要获得由一对迭代器所标示的区间,用来表示操作范围。这一对迭代器所标示的区间都是前闭后开区间,例如[first, last)

3. 迭代器(iterators):容器与算法之间的胶合剂,是所谓的“泛型指针”。共有五种类型,以及其他衍生变化。从实现的角度来看,迭代器是一种将 operator*、operator->、operator++、operator- - 等指针相关操作进行重载的class template。所有STL容器都有自己专属的迭代器,只有容器本身才知道如何遍历自己的元素。原生指针(native pointer)也是一种迭代器。

4. 仿函数(functors):行为类似函数,可作为算法的某种策略(policy)。从实现的角度来看,仿函数是一种重载了operator()的class或class template。一般的函数指针也可视为狭义的仿函数。

5. 配接器(adapters):一种用来修饰容器、仿函数、迭代器接口的东西。例如:STL提供的queue 和 stack,虽然看似容器,但其实只能算是一种容器配接器,因为它们的底部完全借助deque,所有操作都由底层的deque供应。改变 functors接口者,称为function adapter;改变 container 接口者,称为container adapter;改变iterator接口者,称为iterator adapter。

6. 配置器(allocators):负责空间配置与管理。从实现的角度来看,配置器是一个实现了动态空间配置、空间管理、空间释放的class template。

这六大组件的交互关系:container(容器) 通过 allocator(配置器) 取得数据储存空间,algorithm(算法)通过 iterator(迭代器)存取 container(容器) 内容,functor(仿函数) 可以协助 algorithm(算法) 完成不同的策略变化,adapter(配接器) 可以修饰或套接 functor(仿函数)

序列式容器:
vector-数组,元素不够时再重新分配内存,拷贝原来数组的元素到新分配的数组中。
list-单链表。
deque-分配中央控制器 map (并非 map 容器),map 记录着一系列的固定长度的数组的地址.记住这个 map 仅仅保存的是数组的地址,真正的数据在数组中存放着.deque 先从 map 中央的位置(因为双向队列,前后都可以插入元素)找到一个数组地址,向该数组中放入数据,数组不够时继续在 map 中找空闲的数组来存数据。当 map 也不够时重新分配内存当作新的 map,把原来 map 中的内容 copy 的新 map 中。所以使用 deque 的复杂度要大于 vector,尽量使用 vector。

stack-基于deque。
queue-基于deque。
heap-完全二叉树,使用最大堆排序,以数组(vector)的形式存放。
priority_queue-基于heap。
slist-双向链表。

关联式容器:
set,map,multiset,multimap-基于红黑树(RB-tree),一种加上了额外平衡条件的二叉搜索树。

hash table-散列表。将待存数据的key经过映射函数变成一个数组(一般是vector)的索引,例如:数据的key%数组的大小=数组的索引(一般文本通过算法也可以转换为数字),然后将数据当作此索引的数组元素。有些数据的key经过算法的转换可能是同一个数组的索引值(碰撞问题,可以用线性探测,二次探测来解决),STL是用开链的方法来解决的,每一个数组的元素维护一个list,他把相同索引值的数据存入一个list,这样当list比较短时执行删除,插入,搜索等算法比较快。

hash_map,hash_set,hash_multiset,hash_multimap-基于hashtable。

list和vector有什么区别?

vector拥有一段连续的内存空间,因此支持随机存取,如果需要高效的随即存取,而不在乎插入和删除的效率,使用vector。

list拥有一段不连续的内存空间,因此不支持随机存取,如果需要大量的插入和删除,而不关心随即存取,则应使用list。

虚函数

C++的多态分为静态多态(编译时多态)和动态多态(运行时多态)两大类。静态多态通过重载、模板来实现;动态多态就是通过本文的主角虚函数来体现的。	

虚函数实现原理:包括虚函数表、虚函数指针等 

虚函数的作用说白了就是:当调用一个虚函数时,被执行的代码必须和调用函数的对象的动态类型相一致。编译器需要做的就是如何高效的实现提供这种特性。不同编译器实现细节也不相同。大多数编译器通过vtbl(virtual table)和 vptr(virtual table pointer)来实现的。 当一个类声明了虚函数或者继承了虚函数,这个类就会有自己的 vtbl。vtbl 实际上就是一个函数指针数组,有的编译器用的是链表,不过方法都是差不多。vtbl 数组中的每一个元素对应一个函数指针指向该类的一个虚函数,同时该类的每一个对象都会包含一个 vptr,vptr 指向该 vtbl 的地址。

结论

每个声明了虚函数或者继承了虚函数的类,都会有一个自己的vtbl

同时该类的每个对象都会包含一个vptr去指向该vtbl

虚函数按照其声明顺序放于vtbl表中, vtbl数组中的每一个元素对应一个函数指针指向该类的虚函数

如果子类覆盖了父类的虚函数,将被放到了虚表中原来父类虚函数的位置

在多继承的情况下,每个父类都有自己的虚表。子类的成员函数被放到了第一个父类的表中

为什么 C++里访问虚函数比访问普通函数慢?
单继承时性能差不多,多继承的时候会慢。调用虚函数,相比普通函数,实际上多了三条指令:取虚表,取函数地址,call调用。虚函数运行时所需的代价主要是虚函数不能是内联函

调用性能方面

从前面虚函数的调用过程可知。当调用虚函数时过程如下(引自More Effective C++):

通过对象的 vptr 找到类的 vtbl。这是一个简单的操作,因为编译器知道在对象内 哪里能找到 vptr(毕竟是由编译器放置的它们)。因此这个代价只是一个偏移调整(以得到 vptr)和一个指针的间接寻址(以得到 vtbl)。

找到对应 vtbl 内的指向被调用函数的指针。这也是很简单的, 因为编译器为每个虚函数在 vtbl 内分配了一个唯一的索引。这步的代价只是在 vtbl 数组内 的一个偏移。

调用第二步找到的的指针所指向的函数。

在单继承的情况下,调用虚函数所需的代价基本上和非虚函数效率一样,在大多数计算机上它多执行了很少的一些指令,所以有很多人一概而论说虚函数性能不行是不太科学的。在多继承的情况下,由于会根据多个父类生成多个vptr,在对象里为寻找 vptr 而进行的偏移量计算会变得复杂一些,但这些并不是虚函数的性能瓶颈。虚函数运行时所需的代价主要是虚函数不能是内联函。这也是非常好理解的,是因为内联函数是指在编译期间用被调用的函数体本身来代替函数调用的指令,但是虚函数的“虚”是指“直到运行时才能知道要调用的是哪一个函数。”但虚函数的运行时多态特性就是要在运行时才知道具体调用哪个虚函数,所以没法在编译时进行内联函数展开。当然如果通过对象直接调用虚函数它是可以被内联,但是大多数虚函数是通过对象的指针或引用被调用的,这种调用不能被内联。 因为这种调用是标准的调用方式,所以虚函数实际上不能被内联。

占用空间方面

在上面的虚函数实现原理部分,可以看到为了实现运行时多态机制,编译器会给每一个包含虚函数或继承了虚函数的类自动建立一个虚函数表,所以虚函数的一个代价就是会增加类的体积。在虚函数接口较少的类中这个代价并不明显,虚函数表 vtbl 的体积相当于几个函数指针的体积,如果你有大量的类或者在每个类中有大量的虚函数,你会发现 vtbl 会占用大量的地址空间。但这并不是最主要的代价,主要的代价是发生在类的继承过程中,在上面的分析中,可以看到,当子类继承父类的虚函数时,子类会有自己的 vtbl,如果子类只覆盖父类的一两个虚函数接口,子类 vtbl 的其余部分内容会与父类重复。这在如果存在大量的子类继承,且重写父类的虚函数接口只占总数的一小部分的情况下,会造成大量地址空间浪费。在一些 GUI 库上这种大量子类继承自同一父类且只覆盖其中一两个虚函数的情况是经常有的,这样就导致 UI 库的占用内存明显变大。 由于虚函数指针 vptr 的存在,虚函数也会增加该类的每个对象的体积。在单继承或没有继承的情况下,类的每个对象会多一个vptr指针的体积,也就是4个字节;在多继承的情况下,类的每个对象会多N个(N=包含虚函数的父类个数)vptr的体积,也就是4N个字节。当一个类的对象体积较大时,这个代价不是很明显,但当一个类的对象很轻量的时候,如成员变量只有4个字节,那么再加上4(或4N)个字节的 vptr,对象的体积相当于翻了1(或N)倍,这个代价是非常大的。

C++虚函数实现原理浅析

纯虚函数,为什么需要纯虚函数?

纯虚函数是在基类中声明的虚函数,它在基类中没有定义,但要求任何派生类都要定义自己的实现方法。在基类中实现纯虚函数的方法是在函数原型后加 “=0”

virtual void funtion1()=0

原因:
1. 为了方便使用多态特性,我们常常需要在基类中定义虚拟函数。
2. 在很多情况下,基类本身生成对象是不合情理的。例如,动物作为一个基类可以派生出老虎、孔雀等子类,但动物本身生成对象明显不合常理。

为了解决上述问题,引入了纯虚函数的概念,将函数定义为纯虚函数(方法:virtual ReturnType Function()= 0;),则编译器要求在派生类中必须予以重写以实现多态性。同时含有纯虚拟函数的类称为抽象类,它不能生成对象。这样就很好地解决了上述两个问题。声明了纯虚函数的类是一个抽象类。所以,用户不能创建类的实例,只能创建它的派生类的实例。

定义纯虚函数的目的在于,使派生类仅仅只是继承函数的接口。
纯虚函数的意义,让所有的类对象(主要是派生类对象)都可以执行纯虚函数的动作,但类无法为纯虚函数提供一个合理的缺省实现。所以类纯虚函数的声明就是在告诉子类的设计者,“你必须提供一个纯虚函数的实现,但我不知道你会怎样实现它”。

虚函数和纯虚函数的区别

为什么需要虚析构函数,什么时候不需要?父类的析构函数为什么要定义为虚函数

一般情况下类的析构函数里面都是释放内存资源,而析构函数不被调用的话就会造成内存泄漏。这样做是为了当用一个基类的指针删除一个派生类的对象时,派生类的析构函数会被调用。

当然,并不是要把所有类的析构函数都写成虚函数。因为当类里面有虚函数的时候,编译器会给类添加一个虚函数表,里面来存放虚函数指针,这样就会增加类的存储空间。所以,只有当一个类被用来作为基类的时候,才把析构函数写成虚函数。
```

#### 内联函数、构造函数、静态成员函数可以是虚函数吗?
```cpp
inline, static, constructor三种函数都不能带有virtual关键字。

inline是编译时展开,必须有实体;

static属于class自己的,也必须有实体;

virtual函数基于vtable(内存空间),constructor函数如果是virtual的,调用时也需要根据vtable寻找,但是constructor是virtual的情况下是找不到的,因为constructor自己本身都不存在了,创建不到class的实例,没有实例,class的成员(除了public static/protected static for friend class/functions,其余无论是否virtual)都不能被访问了。

虚函数实际上不能被内联:虚函数运行时所需的代价主要是虚函数不能是内联函。这也是非常好理解的,是因为内联函数是指在编译期间用被调用的函数体本身来代替函数调用的指令,但是虚函数的“虚”是指“直到运行时才能知道要调用的是哪一个函数。”但虚函数的运行时多态特性就是要在运行时才知道具体调用哪个虚函数,所以没法在编译时进行内联函数展开。当然如果通过对象直接调用虚函数它是可以被内联,但是大多数虚函数是通过对象的指针或引用被调用的,这种调用不能被内联。 因为这种调用是标准的调用方式,所以虚函数实际上不能被内联。

构造函数不能是虚函数。而且,在构造函数中调用虚函数,实际执行的是父类的对应函数,因为自己还没有构造好, 多态是被disable的。 

静态的对象是属于整个类的,不对某一个对象而言,同时其函数的指针存放也不同于一般的成员函数,其无法成为一个对象的虚函数的指针以实现由此带来的动态机制。			
```

#### 最后,总结一下关于虚函数的一些常见问题
````cpp
1) 虚函数是动态绑定的,也就是说,使用虚函数的指针和引用能够正确找到实际类的对应函数,而不是执行定义类的函数。这是虚函数的基本功能,就不再解释了。 

2) 构造函数不能是虚函数。而且,在构造函数中调用虚函数,实际执行的是父类的对应函数,因为自己还没有构造好, 多态是被disable的。 

3) 析构函数可以是虚函数,而且,在一个复杂类结构中,这往往是必须的。

4) 将一个函数定义为纯虚函数,实际上是将这个类定义为抽象类,不能实例化对象。 

5) 纯虚函数通常没有定义体,但也完全可以拥有。

6)  析构函数可以是纯虚的,但纯虚析构函数必须有定义体,因为析构函数的调用是在子类中隐含的。 

7) 非纯的虚函数必须有定义体,不然是一个错误。 

8) 派生类的override虚函数定义必须和父类完全一致。除了一个特例,如果父类中返回值是一个指针或引用,子类override时可以返回这个指针(或引用)的派生。

虚析构函数(√)、纯虚析构函数(√)、虚构造函数(X)

为什么需要虚继承?虚继承实现原理解析,

虚拟继承是多重继承中特有的概念。虚拟基类是为解决多重继承而出现的。
如:类D继承自类B1、B2,而类B1、B2都继 承自类A,因此在类D中两次出现类A中的变量和函数(菱形继承)。为了节省内存空间,可以将B1、B2对A的继承定义为虚拟继承,而A就成了虚拟基类,虚拟继承在一般的应用中很少用到,所以也往往被忽视,这也主要是因为在C++中,多重继承是不推荐的,也并不常用,而一旦离开了多重继承,虚拟继承就完全失去了存在的必要因为这样只会降低效率和占用更多的空间。

虚继承的特点是,在任何派生类中的virtual基类总用同一个(共享)对象表示

内存分配

内存分配方式

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

2. 在栈上创建。这是出现最多的情况,在程序中 int a;这种情况就是在栈上为 a 分配内存。在执行函数时,函数内局部变量都可以在栈上创建,函数执行结束时这些存储单元被自动释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。

3. 从堆上分配,亦称为动态内存分配。程序在运行中使用 malloc 和 new 申请任意多少的内存,同时由程序员自己手动使用 free 和 delete 释放内存。动态内存的生存周期由程序员决定。

常见的内存错误

内存分配未成功,却使用了它

在使用内存之前检查指针是否为 NULL 。如果指针 p 是函数的参数,那么在函数的入口处用 assert (p!=NULL) 进行检查。如果是用 malloc 或 new 来申请内存,应该用 if(p == NULL) 或 if( p !=NULL ) 进行防错处理

内存分配虽然成功,但是尚未初始化就引用它

犯这种错误主要有两个起因:一是没有初始化的观念;二是误以为内存的缺省初值全为零,导致引用初值错误(例如数组)

内存的缺省初值究竟是什么并没有统一的标准,尽管有些时候为零值,宁可信其无不可信其有。所以无论用何种方式创建数组,都必须赋初值,即便是赋零值也不可省略

内存分配成功并且已经初始化,但操作越过了内存的边界

例如在使用数组时经常发生下标“多1 ”或者“少1 ”的操作。特别是在for 循环语句中,循环次数很容易搞错,导致数组操作越界

忘记了释放内存,造成内存泄露

含有这种错误的函数每被调用一次就丢失一块内存。刚开始时系统的内存充足,你看不到错误。终有一次程序突然死掉,系统出现提示:内存耗尽

动态内存的申请与释放必须配对,程序中 malloc 与 free 的使用次数一定要相同,否则肯定有错误(new/delete 同理)

释放了内存却继续使用它

问题出现的情况:
1. 程序中的对象调用关系过于复杂,实在难以搞清楚某个对象究竟是否已经释放了内存,此时应该重新设计数据结构,从根本上解决对象管理的混乱局面
 
2. 函数的return 语句写错了,注意不要返回指向“栈内存”的“指针”或者“引用”,因为该内存在函数体结束时被自动销毁

3. 使用free 或delete 释放了内存后,没有将指针设置为NULL。导致产生“野指针”
 
4. 在执行拷贝构造的时候发生了浅拷贝,多次释放了同一指针

总结如下

1. 用malloc 或new 申请内存之后,应该立即检查指针值是否为NULL。防止使用指针值为NULL的内存

2. 不要忘记为数组和动态内存赋初值。防止将未被初始化的内存作为右值使用

3. 避免数组或指针的下标越界,特别要当心发生“多1 ”或者“少1 ”操作

4. 动态内存的申请与释放必须配对,防止内存泄漏

5. 用free 或delete 释放了内存之后,立即将指针设置为NUL L ,防止产生“野指针”

6. 当类内含有堆上的内存时,一定要考虑深拷贝问题

一些区别总结

指针和引用的区别

1. 指针指向一块内存,它的内容是指向内存的地址;引用是某内存的别名

2. 引用使用是无需解引用,指针需解引用

3. 引用不能为空,指针可以为空

4. 引用在定义是被初始化一次,之后不可变;指针可变

5. 程序为指针变量分配内存区域,而引用不需要分配内存区域

6. 引用的底层实现是一个指针常量 Type& name <===> Type* const name

memcpy和strcpy的区别

1. memcpy 用来内存拷贝的,它有指定的拷贝数据长度,他可以拷贝任何数据类型的对象

2. strcpy 它只能去拷贝字符串,它遇到 '\0' 结束拷贝

new和malloc的区别,free和delete的区别

1. malloc 与 free 是 C 语言的标准库函数,new/delete 是 C++ 的运算符。它们都可用于申请动态内存和释放内存。

2. 对于非内部数据类型的对象而言,光用 maloc/free 无法满足动态对象的要求。对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数。由于 malloc/free 是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于 malloc/free。

struct和class的区别

1. 成员变量
结构在默认情况下的成员是公共(public)的,
而类在默认情况下的成员是私有(private)的。

2. 存储
struct保证成员按照声明顺序在内存中存储。class不保证

3. 继承
struct A {};
class B : A{}; //private继承
struct C : B{}; //public继承
这是由于class默认是private,struct默认是public。

struct与union的区别.(假定在32位机器上)

1. 一个union类型的变量,所有成员变量共享一块内存,该内存的大小有这些成员变量中长度最大的一个来决定,struct 中成员变量内存都是独立的

2. union分配的内存是连续的,而struct不能保证分配的内存是连续的

main 函数执行以前,还会执行什么代码

全局对象的构造函数会在main 函数之前执行。

局部变量能否和全局变量重名?

能,局部会屏蔽全局。要用全局变量,需要使用”::”

局部变量可以与全局变量同名,在函数内引用这个变量时,会用到同名的局部变量,而不会用到全局变量。对于有些编译器而言,在同一个函数内可以定义多个同名的局部变量,比如在两个循环体内都定义一个同名的局部变量,而那个局部变量的作用域就在那个循环体内

描述内存分配方式以及它们的区别

1. 从静态存储区域分配。内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。例如全局变量,static 变量。

2. 在栈上创建。在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集。

3. 从堆上分配,亦称动态内存分配。程序在运行的时候用malloc 或new 申请任意多少的内存,程序员自己负责在何时用free 或delete 释放内存。动态内存的生存期由程序员决定,使用非常灵活,但问题也最多。

类成员函数的重载、重写(覆盖)和重定义(隐藏)区别

重载(添加):
1. 相同的范围(在同一个类中)
2. 函数名字相同
3. 参数不同
4. virtual 关键字可有可无

重写(覆盖):是指派生类函数覆盖基类函数,特征是:
1. 不同的范围,分别位于基类和派生类中,通常在多态下,子类重写父类中的函数
2. 函数名字相同
3. 参数相同
4. 基类函数必须有 virtual 关键字

重定义(隐藏):是指派生类函数覆盖基类函数,特征是:
1. 如果派生类的函数和基类的函数同名,但是参数不同,此时,不管有无 virtual,基类的函数被隐藏。
2. 如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有 virtual 关键字,此时,基类的函数会被隐藏。

const与#define 相比,有何优点

const与#define相比,区别和优点

1. const 常量有数据类型,而宏常量没有数据类型。编译器可以对前者进行类型安全检查。而对后者只进行字符替换,没有类型安全检查,并且在字符替换可能会产生意料不到的错误

2. 有些集成化的调试工具可以对const 常量进行调试,但是不能对宏常量进行调试

3. const可节省空间,避免不必要的内存分配,提高效率

数组与指针的区别

数组和指针的区别与联系

1. 数组要么在静态存储区被创建(如全局数组),要么在栈上被创建。指针可以随时指向任意类型的内存块。

2. 同类型指针变量可以相互赋值,数组不行,只能一个一个元素的赋值或拷贝

变量声明和定义的区别

为变量分配地址和存储空间的称为定义,不分配地址的称为声明。一个变量可以在多个地方声明,但是只在一个地方定义。加入extern修饰的是变量的声明,说明此变量将在文件以外或在文件后面部分定义

sizeof和strlen的区别

1. sizeof是一个操作符,strlen是库函数

2. sizeof的参数可以是数据的类型,也可以是变量,而strlen只能以结尾为‘\0‘的字符串作参数

3. 编译器在编译时就计算出了sizeof的结果。而strlen 函数必须在运行时才能计算出来。并且sizeof计算的是数据类型占内存的大小,而strlen计算的是字符串实际的长度

4. 数组做sizeof的参数不退化,传递给strlen就退化为指针了