C++基础知识归纳(1)-大厂必备八股文篇

共享数据的保护:

  1. 常引用:使所引用的形参不能被更新
    void display(const double& a);
  2. 常对象:在生存期内不能被更新,但必须被初始化
    A const a(3,4);
  3. 常成员函数:不能修改对象中数据成员,也不能调用类中没有被const 修饰的成员函数(常对象唯一的对外接口).如果声明了一个常对象,则该对象只能调用他的常函数!->可以用于对重载函数的区分;
    void print();
    void print() const;
  4. extern int a:使其他文件也能访问该变量
    声明一个函数或定义函数时,冠以static的话,函数的作用域就被限制在了当前编译单元,当前编译单元内也必须包含函数的定义,也只在其编译单元可见,其他单元不能调用这个函数(每一个cpp 文件就是一个编译单元)。

运算符重载注意

  • 单目运算符最好重载为成员函数,双目最好为友元函数。
  • =、[]只能重载成员函数,<<、>>只能重载为友元函数。

程序内存分配方式以及它们的区别

内存分配大致上可以分成5块:

  1. 栈区(stack)。栈,就是那些由编译器在需要时分配,在不需
    要的时候自动清除的变量的存储区。里面的变量通常是局部变量、函数参数等。(由编译器管理)
  2. 堆区(heap)。一般由程序员分配、释放,若程序员不是放,程
    序结束时可能由系统回收。注意,它与数据结构中的堆是两回事,分配方式类似于链表。
  3. 全局区(静态区)(static)。全局变量和静态变量被分配到同
    一块内存中。程序结束后由系统释放。
  4. 常量存储区。常量字符串就是放在这里的,不允许修改,程序
    结束后由系统释放。
  5. 程序代码区。存放函数体的二进制代码。

全局变量与全局静态变量的区别

  1. 若程序由一个源文件构成时,全局变量与全局静态变量没有区别。
  2. 若程序由多个源文件构成时,全局变量与全局静态变量不同:全局静态变量使得该变量成为定义该变量的源文件所独享,即:全局静态变量对组成该程序的其它源文件是无效的。
  3. 具有外部链接的静态,可以在所有源文件里调用,除了本文件,其他文件可以通过extern的方式引用。
    ----静态变量只被所属源文件使用

new delete 与malloc free 的联系与区别

  • new delete和malloc free都是释放申请的堆上的空间,都是成对存在的,否则将会造成内存泄露或二次释放
  • 不同的是,new delete是C++中定义的操作符,new除了分配空间外,还会调用类的构造函数来完成初始化工作,delete除了释放空间外还会调用类的析构函数。而malloc和free是C语言中定义的函数。

explicit

函数声明时加上explicit可以阻止函数参数被隐式转换。

Class A
{
   explicit A(int a);
}

Void main()
{
   A a1=12;   //不加explicit时会被隐式转换位 A a1=A(12);加了此时编译器会报错。
}

被声明为explicit的构造函数通常比non-explicit 函数更受欢迎。

mutable关键字

mutalbe的中文意思是“可变的,易变的”,跟constant(既C++中的const)是反义词。在C++中,mutable也是为了突破const的限制而设置的。被mutable修饰的变量(mutable只能由于修饰类的非静态数据成员),将永远处于可变的状态,即使在一个const函数中。

我们知道,假如类的成员函数不会改变对象的状态,那么这个成员函数一般会声明为const。但是,有些时候,我们需要在const的函数里面修改一些跟类状态无关的数据成员,那么这个数据成员就应该被mutalbe来修饰。(使用mutable修饰的数据成员可以被const成员函数修改)。

用const修饰函数的返回值

如果给以“指针传递”方式的函数返回值加const修饰,那么函数返回值(即指针)的内容不能被修改,该返回值只能被赋给加const修饰的同类型指针。例如函数

Const char * GetString(void);
// 如下语句将出现编译错误:
char*str = GetString();
// 正确的用法是
Const char *str =GetString();

一般只在返回值为引用和指针时使用,返回其他值时没有这个必要。

宏、const和enum

  1. #define 不被视为语言的一部分。对于单纯常量,最好用const对象或者enum替换#define。
  2. 对于类似函数的宏,尽量使用内联函数替换掉#define。

stack的生存期

C++中的static对象是指存储区不属于stack和heap、"寿命"从被构造出来直至程序结束为止的对象。这些对象包括全局对象,定义于namespace作用域的对象,在class、function以及file作用域中被声明为static的对象。其中,函数内的static对象称为local static对象,而其它static对象称为non-local static对象。

这两者在何时被初始化(构造)这个问题上存在细微的差别

  • 对于local static对象,在其所属的函数被调用之前,该对象并不存在,即只有在第一次调用对应函数时,local static对象才被构造出来。
  • 而对于non-local static对象,在main()函数开始前就已经被构造出来,并在main()函数结束后被析构。

建议:

  1. 对内置对象进行手工初始化,因为C++不保证初始化它们。
  2. 构造函数最好使用成员初值列,而不要在构造函数本体中使用赋值操作。初值列中列出的成员变量,其排序次序应该和它们在class中的声明次序相同(初始化顺序与声明变量顺序一致)。
  3. 为免除“跨编译单元的初始化次序问题”,尽量以local static对象替换non-local static对象。

STL相关

  1. STL被组织成13个头文件。algorithm、deque、functional、iterator、vector、list、map、memory、numeric、queue、set、stack 和 utility。
  2. STL是C++通用库,由容器,算法,迭代器,仿函数,内存配置器构成。
  3. 容器
    作为STL的最主要组成部分--容器,分为向量(vector),双端队列(deque),表(list),队列(queue),堆栈(stack),集合(set),多重集合(multiset),映射(map),多重映射(multimap)。

关联式容器:map,multimap和set,multiset(multi代表允许重复元素)。
< map>中包含 pair<typename T1,typename T2>这种对组的结构体。
函数get_allocator()用于获取map或multimap的内存配置器,内存配置器类似于指针的首地址。

MAP::allocator_type m1_alloc;
MAP m1;
m1_alloc=m1.get_allocator();
int ctg=count_if(myvector.begin(),myvector.end(),bind2nd(greater<int>(),2))
bind2nd(greater<int>(),2)表示数值大于2的情况,为真时才执行
//类似的:
find_if(myvector.begin(),myvector.end(),bind2nd(greater<int>(),3))
  1. 算法
    算法部分主要由头文件< algorithm>,< numeric>和组成。< algorithm>是所有STL头文件中最大的一个,它是由一大堆模版函数组成的,可以认为每个函数在很大程度上都是独立的,其中常用到的功能范 围涉及到比较、交换、查找、遍历操作、复制、修改、移除、反转、排序、合并等等。体积很小,只包括几个在序列上面进行简单数学运算的模板函数,包括加法和乘法在序列上的一些操作。中则定义了一些模板类,用以声明函数对象。
  • auto_ptr只能对new分配的内存使用,不能对new[]的使用。
  • algorithm 中的for_each(T.begin(),T.end(),function);find(T.begin(),T.end(),value)
  • T.reserve(x):预留空间。实际空间小于它则扩充
  1. 迭代器
    在头文件 iterator中,迭代器类型:输入型迭代器,输出型迭代器,前向迭代器(前两者结合),双向迭代器(可回头),随机存取迭代器。

迭代配接器是特殊的迭代器,可使算法能够以逆向,安插模式进行工作。
比如:

  • 逆向迭代器:T.rbegin(),T.rend(),rbegin()指向最后一个元素,++操作逐次向前。
  • 插入型迭代器:T.front_inserter(),T.back_inserter(),
    copy(d1.begin(),d1.end(),inserter(d2,d2.begin()):把每个元素都插在前一个元素的前面
  • 流迭代器:copy(d1.begin(),d1.end(),ostream_iterator(cout,”, ”)):把d1复制到输出流上输出,每个元素以,隔开。
  • 迭代器辅助函数:advance():使迭代器前进或后退。如advance(it,-1);
    distance():求两个迭代器之间的距离,要求同类型而且前者可以自增到后者
    iter_swap():交换两个迭代器的所知内容。
    在这里插入图片描述
  1. 仿函数
    就是函数对象,类重载了操作符 ();
  2. 适配器
    适配器是用来修改其他组件接口的STL组件,是带有一个参数的类模板(这个参数是操作的值的数据类型)。STL定义了3种形式的适配器:容器适配器,迭代器适配器,函数适配器。
  • 容器适配器:包括栈(stack)、队列(queue)、优先(priority_queue)。使用容器适配器,stack就可以被实现为基本容器类型(vector,dequeue,list)的适配。可以把stack看作是某种特殊的vctor,deque或者list容器,只是其操作仍然受到stack本身属性的限制。queue和priority_queue与之类似。容器适配器的接口更为简单,只是受限比一般容器要多在这里插入图片描述
  • 迭代器适配器:修改为某些基本容器定义的迭代器的接口的一种STL组件。反向迭代器和插入迭代器都属于迭代器适配器,迭代器适配器扩展了迭代器的功能。
  • 函数适配器:通过转换或者修改其他函数对象使其功能得到扩展。这一类适配器有否定器(相当于"非"操作)、绑定器、函数指针适配器。函数对象适配器的作用就是使函数转化为函数对象,或是将多参数的函数对象转化为少参数的函数对象。
  1. 空间/内存配置器(Allocator)
    分为一级配置器和二级配置器;
  • 一级配置器:当申请空间大小超过128bytes时,直接调用malloc/free进行分配和释放
  • 二级适配器:当申请空间小于128bytes时使用
    在这里插入图片描述
    首先会调用ROUND_UP(),这个是将要申请的内存字节数上调为8的倍数,然后再去内存池中获取分配的空间;

STL的分配器(allocaotr)用于封装STL容器在内存管理上的底层细节。在C++中,其内存配置和释放如下:

  • new运算分两个阶段:(1)调用::operator new配置内存;(2)调用对象构造函数构造对象内容
  • delete运算分两个阶段:(1)调用对象希构函数;(2)掉员工::operator delete释放内存
    为了精密分工,STL allocator将两个阶段操作区分开来:内存配置有alloc::allocate()负责,内存释放由alloc::deallocate()负责;对象构造由::construct()负责,对象析构由::destroy()负责。
  • 同时为了提升内存管理的效率,减少申请小内存造成的内存碎片问题,SGI STL采用了两级配置器,当分配的空间大小超过128B时,会使用第一级空间配置器;当分配的空间大小小于128B时,将使用第二级空间配置器。第一级空间配置器直接使用malloc()、realloc()、free()函数进行内存空间的分配和释放,而第二级空间配置器采用了内存池技术,通过空闲链表来管理内存。

程序编译

  • 程序编译是指将编译好的源文件翻译成二进制目标代码的过程。主要是检查语法错误,正确的源程序文件经过编译后在磁盘上生成目标文件。编译产生的目标文件是可重定位的程序模块,不能直接运行。链接则是把目标文件和其他分别进行编译生成的目标程序模块以及系统提供的标准库函数链接在一起,生成可运行的可执行文件。

const 赋值类型不一致时,生成新的常量

Char a=’c’;
Char& b=a;
Const int& rc=a;
b=’x’;

const引用类型初始化时前后数据类型不一致时,生成一个新的只读类型。此时b改变a的值时,rc所指的值不变。

C++不支持引用数组

C++单例模式示例(多线程下)

class danli
{
private:
	danli()
	{

	}
	static danli* instance;
public:
	static danli* getInstance()  
	{
        if (instance == NULL) // instance == NULL不代表instance一定没被new过
        {
           // 双重判断,避免多线程环境下每次调用getInstance()函数都会先加锁再判断
           // 只有在instance == NULL时再加锁去申请对象
            unique_lock<muytex> mymute(resource_mutex);
		    if (instance == NULL){
		        instance = new danli();
		        static huishou h1;
		    }
        }
		return instance;
	}

	class huishou
	{
	public:
		~huishou()
		{
			if (instance)
			{
				delete instance;
				instance = NULL;
			}
		}
	};

静态成员变量在main()之前创建,再main()之后回收。

位运算

  1. 左移运算符m << n表示把m左移n位。在左移n位的时候,最左边的n位将被丢弃,同时在最右边补上n个0;
    00001010 << 2 = 00101000
  2. 右移运算符m >> n表示把m右移n位。在右移n位的时候,最右边的n位将被丢弃,但对有符号和无符号数时,最左边添加的数字不同;
    如果是无符号数,则最左边的n位用0替代;
    如果是有符号数,则用符号位替代;
    10001010 >> 3 = 11110001
    00001010 >> 3 = 00000001
    在实际运用中,负数实际上是用绝对值的补码进行存储和表示的;
    例如:整数-13的二进制数中含1的位数有30个(int 4个字节,13=00…1101,补码为11…0011);

C++二维数组作为形参传递参数在这里插入图片描述在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

全局变量和static变量的区别

全局变量本身就是静态存储方式, 静态全局变量当然也是静态存储方式。 这两者在存储方式上并无不同。这两者的区别在于非静态全局变量的作用域是整个源程序, 当一个源程序由多个源文件组成时,非静态的全局变量在各个源文件中都是有效的。 而静态全局变量则限制了其作用域, 即只在定义该变量的源文件内有效, 在同一源程序的其它源文件中不能使用它。由于静态全局变量的作用域局限于一个源文件内,只能为该源文件内的函数公用, 因此可以避免在其它源文件中引起错误。

为什么栈要比堆速度要快

  • 首先, 栈是本着LIFO原则的存储机制, 对栈数据的定位相对比较快速, 而堆则是随机分配的空间, 处理的数据比较多, 无论如何, 至少要两次定位.
  • 其次, 栈是由CPU提供指令支持的, 在指令的处理速度上, 对栈数据进行处理的速度自然要优于由操作系统支持的堆数据.
  • 再者, 栈是在一级缓存中做缓存的, 而堆则是在二级缓存中, 两者在硬件性能上差异巨大.
  • 最后, 各语言对栈的优化支持要优于对堆的支持, 比如swift语言中, 三个字及以内的struct结构, 可以在栈中内联, 从而达到更快的处理速度.

c++ 析构函数调用时间

  1. 对象生命周期结束,被销毁时
  2. delete指向对象的指针时,或delete指向对象的基类类型指针,而其基类虚构函数是虚函数时
  3. 对象i是对象o的成员,o的析构函数被调用时,对象i的析构函数也被调用

c++中关于cout与printf的简单区别

在这里插入图片描述
在这里插入图片描述
详细请参考:c++中关于cout与printf的简单区别

class与struct区别

C++中,class与struct都可以定义一个类。他们有以下两点区别:

  1. 默认继承权限,如果不指定,来自class的继承按照private继承处理,来自struct的继承按照public继承处理;
  2. 成员的默认访问权限。class的成员默认是private权限,struct默认是public权限。
    以上两点也是struct和class最基本的差别,也是最本质的差别;

静态绑定 动态绑定 (也叫动态连编,静态连编)

如果父类中存在有虚函数,那么编译器便会为之生成虚表(属于类)与虚指针(属于某个对象),在程序运行时,根据虚指针的指向,来决定调用哪个虚函数,这称之与动态绑定,与之相对的是静态绑定,静态绑定在编译期就决定了。

  1. class和template都支持接口与多态;
  2. 对classes而言,接口是显式的,以函数签名为中心。多态则是通过virtual函数发生于运行期;
  3. 对template参数而言,接口是隐式的,奠基于有效表达式。多态则是通过template具现化和函数重载解析发生于编译期。
  4. 泛型
    泛型是通过参数化类型来实现在同一份代码上操作多种数据类型。利用“参数化类型”将类型抽象化,从而实现灵活的复用。

C/C++中指针和引用的区别

  1. 指针有自己的一块空间,指针是一个变量,只不过这个变量存储的是一个地址,指向内存的一个存储单元,即指针是一个实体。而引用只是一个别名;
  2. 使用sizeof看一个指针的大小是4,而引用则是被引用对象的大小;
  3. 指针可以被初始化为NULL,而引用必须被初始化且必须是一个已有对象 的引用;
  4. 作为参数传递时,指针需要被解引用才可以对对象进行操作,而直接对引 用的修改都会改变引用所指向的对象;
  5. 可以有const指针,但是没有const引用;
  6. 指针在使用中可以指向其它对象,但是引用只能是一个对象的引用,不能 被改变;
  7. 指针可以有多级指针(**p),而引用至于一级;
  8. 指针和引用使用++运算符的意义不一样;
  9. 如果返回动态内存分配的对象或者内存,必须使用指针,引用可能引起内存泄露。

c++里面的四种智能指针以及代码实现

  • 为什么要使用智能指针:
    智能指针的作用是管理一个指针,因为存在以下这种情况:申请的空间在函数结束时忘记释放,造成内存泄漏。使用智能指针可以很大程度上的避免这个问题,因为智能指针就是一个类,当超出了类的作用域是,类会自动调用析构函数,析构函数会自动释放资源。所以智能指针的作用原理就是在函数结束时自动释放内存空间,不需要手动释放内存空间。
  1. auto_ptr(c++98的方案,cpp11已经抛弃)
    采用所有权模式。
auto_ptr< string> p1 (new string ("I reigned lonely as a cloud.));
auto_ptr<string> p2;
p2 = p1; //auto_ptr不会报错.

此时不会报错,p2剥夺了p1的所有权,但是当程序运行时访问p1将会报错。所以auto_ptr的缺点是:存在潜在的内存崩溃问题!

  1. unique_ptr(替换auto_ptr)
    unique_ptr实现独占式拥有或严格拥有概念,保证同一时间内只有一个智能指针可以指向该对象。它对于避免资源泄露(例如“以new创建对象后因为发生异常而忘记调用delete”)特别有用。

采用所有权模式,还是上面那个例子

unique_ptr<string> p3 (new string ("auto"));   //#4
unique_ptr<string> p4;                       //#5
p4 = p3;//此时会报错!!

编译器认为p4=p3非法,避免了p3不再指向有效数据的问题。因此,unique_ptr比auto_ptr更安全。

  1. shared_ptr
    shared_ptr实现共享式拥有概念。多个智能指针可以指向相同对象,该对象和其相关资源会在“最后一个引用被销毁”时候释放。从名字share就可以看出了资源可以被多个指针共享,它使用计数机制来表明资源被几个指针共享。可以通过成员函数use_count()来查看资源的所有者个数。除了可以通过new来构造,还可以通过传入auto_ptr, unique_ptr,weak_ptr来构造。当我们调用release()时,当前指针会释放资源所有权,计数减一。当计数等于0时,资源会被释放。
  • shared_ptr 是为了解决 auto_ptr 在对象所有权上的局限性(auto_ptr 是独占的), 在使用引用计数的机制上提供了可以共享所有权的智能指针。
  • 成员函数
    use_count 返回引用计数的个数
    unique 返回是否是独占所有权( use_count 为 1)
    swap 交换两个 shared_ptr 对象(即交换所拥有的对象)
    reset 放弃内部对象的所有权或拥有对象的变更, 会引起原有对象的引用计数的减少
    get 返回内部对象(指针), 由于已经重载了()方法, 因此和直接使用对象是一样的.如 shared_ptr sp(new int(1)); sp 与 sp.get()是等价的
  1. weak_ptr
    weak_ptr 是一种不控制对象生命周期的智能指针, 它指向一个 shared_ptr 管理的对象. 进行该对象的内存管理的是那个强引用的 shared_ptr. weak_ptr只是提供了对管理对象的一个访问手段。weak_ptr 设计的目的是为配合 shared_ptr 而引入的一种智能指针来协助 shared_ptr 工作, 它只可以从一个 shared_ptr 或另一个 weak_ptr 对象构造, 它的构造和析构不会引起引用记数的增加或减少。weak_ptr是用来解决shared_ptr相互引用时的死锁问题,如果说两个shared_ptr相互引用,那么这两个指针的引用计数永远不可能下降为0,资源永远不会释放。它是对对象的一种弱引用,不会增加对象的引用计数,和shared_ptr之间可以相互转化,shared_ptr可以直接赋值给它,它可以通过调用lock函数来获得shared_ptr。
    详细请参阅:四种智能指针以及代码实现 / 谈谈智能指针

虚函数和多态

在这里插入图片描述

extern "C"的主要作用简单解释

  • extern "C"的主要作用就是为了能够正确实现C++代码调用其他C语言代码。加上extern "C"后,会指示编译器这部分代码按C语言(而不是C++)的方式进行编译。由于C++支持函数重载,因此编译器编译函数的过程中会将函数的参数类型也加到编译后的代码中,而不仅仅是函数名;而C语言并不支持函数重载,因此编译C语言代码的函数时不会带上函数的参数类型,一般只包括函数名。

  • 这个功能十分有用处,因为在C++出现以前,很多代码都是C语言写的,而且很底层的库也是C语言写的,为了更好的支持原来的C代码和已经写好的C语言库,需要在C++中尽可能的支持C,而extern "C"就是其中的一个策略。

请你说说C语言是怎么进行函数调用的

每一个函数调用都会分配函数栈,在栈内进行函数执行过程。调用前,先把返回地址压栈,然后把当前函数的esp指针压栈。(ESP(Extended Stack Pointer)为扩展栈指针寄存器,是指针寄存器的一种,用于存放函数栈顶指针)

C语言参数压栈顺序?:从右到左

C++中拷贝赋值函数的形参能否进行值传递?

不能。如果是这种情况下,调用拷贝构造函数的时候,首先要将实参传递给形参,这个传递的时候又要调用拷贝构造函数(aa = ex.aa; //此处调用拷贝构造函数)。。如此循环,无法完成拷贝,栈也会满。

include头文件的顺序以及双引号””和尖括号<>的区别

编译器预处理阶段查找头文件的路径不一样
  1. 使用双引号包含的头文件,查找头文件路径的顺序为:
    当前头文件目录
    编译器设置的头文件路径(编译器可使用-I显式指定搜索路径)
    系统变量CPLUS_INCLUDE_PATH/C_INCLUDE_PATH指定的头文件路径
  2. 对于使用尖括号包含的头文件,查找头文件的路径顺序为:
    编译器设置的头文件路径(编译器可使用-I显式指定搜索路径)
    系统变量CPLUS_INCLUDE_PATH/C_INCLUDE_PATH指定的头文件路径

STL中迭代器有什么作用作用,有指针为何还要迭代器

  1. 迭代器
    Iterator(迭代器)模式又称游标(Cursor)模式,用于提供一种方法顺序访问一个聚合对象中各个元素, 而又不需暴露该对象的内部表示。
    或者这样说可能更容易理解:Iterator模式是运用于聚合对象的一种模式,通过运用该模式,使得我们可以在不知道对象内部表示的情况下,按照一定顺序(由iterator提供的方法)访问聚合对象中的各个元素。
    由于Iterator模式的以上特性:与聚合对象耦合,在一定程度上限制了它的广泛运用,一般仅用于底层聚合支持类,如STL的list、vector、stack等容器类及ostream_iterator等扩展iterator。
  2. 迭代器和指针的区别
    迭代器不是指针,是类模板,表现的像指针。他只是模拟了指针的一些功能,通过重载了指针的一些操作符,->、、++、–等。迭代器封装了指针,是一个“可遍历STL( Standard Template Library)容器内全部或部分元素”的对象, 本质是封装了原生指针,是指针概念的一种提升,提供了比指针更高级的行为,相当于一种智能指针,他可以根据不同类型的数据结构来实现不同的++,–等操作。
    迭代器返回的是对象引用而不是对象的值,所以cout只能输出迭代器使用取值后的值而不能直接输出其自身。
  3. 迭代器产生原因
    Iterator类的访问方式就是把不同集合类的访问逻辑抽象出来,使得不用暴露集合内部的结构而达到循环遍历集合的效果。

一个C++源文件从文本到可执行文件经历的过程

对于C/C++编写的程序,从源代码到可执行文件,一般经过下面四个步骤:

  1. 预编译,预编译的时候做一些简单的文本替换,比如宏替换,而不进行语法的检查;
  2. 编译,在编译阶段,编译器将检查一些语法错误,但是,如果使用的函数事先没有定义这种情况,不再这一阶段检查,编译后,得到.s文件
  3. 汇编,将C/C++代码变为汇编代码,得到.o或者.obj文件
  4. 链接,将所用到的外部文件链接在一起,在这一阶段,就会检查使用的函数有没有定义,链接过后,形成可执行文件.exe
    详细请参阅:一个C++源文件从文本到可执行文件经历的过程

内存泄漏原因和判断方法

内存泄漏通常是因为调用了malloc/new等内存申请操作,但是缺少了对应的free/delete。
为了判断内存是否泄漏,我们一方面可以使用Linux环境下的内存泄漏检查工具Valgrind,另一方面我们写代码的时候,可以添加内存申请和释放的统计功能,统计当前申请和释放的内存是否一致,以此来判断内存是否有泄漏。

内存泄漏分类:

  1. 堆内存泄漏(heap leak)。堆内存值得是程序运行过程中根据需要分配通过malloc\realloc\new等从堆中分配的一块内存,再完成之后必须要通过调用对应的free或者delete删除。
    如果程序的设计的错误导致这部分内存没有被释放,那么此后这块内存将不会被使用,就会产生Heap Leak。
  2. 系统资源泄露(Resource Leak)。主要指程序使用系统分配的资源比如 Bitmap,handle,SOCKET等没有使用相应的函数释放掉,导致系统资源的浪费,严重可导致系统效能降低,系统运行不稳定。
  3. 没有将基类的析构函数定义为虚函数。当基类指针指向子类对象时,如果基类的析构函数不是virtual,那么子类的析构函数将不会被调用,子类的资源没有正确的释放,从而造成内存泄漏。

在这里插入图片描述在这里插入图片描述
详细请查阅: C/C++内存泄漏及检测

new和malloc的区别

  1. new分配内存按照数据类型进行分配,malloc分配内存按照指定的大小分配;
  2. new返回的是指定对象的指针,而malloc返回的是void*,因此malloc的返回
    值一般都需要进行类型转化。
  3. new不仅分配一段内存,而且会调用构造函数,malloc不会。
  4. new分配的内存要用delete销毁,malloc要用free来销毁;delete销毁的时候
    会调用对象的析构函数,而free则不会。
  5. new是一个操作符可以重载,malloc是一个库函数。
  6. malloc分配的内存不够的时候,可以用realloc扩容。new没用这样操作。
  7. new如果分配失败了会抛出bad_malloc的异常,而malloc失败了会返回NULL。
  8. 申请数组时:new[]一次分配所有内存,多次调用构造函数,搭配使用delete[]
    ,delete[]多次调用析构函数,销毁数组中的每个对象。而malloc则只能sizeof(int) * n。

段错误的产生原因

1. 段错误是什么

一句话来说,段错误是指访问的内存超出了系统给这个程序所设定的内存空间,例如访问了不存在的内存地址、访问了系统保护的内存地址、访问了只读的内存地址等等情况。这里贴一个对于“段错误”的准确定义。

2.段错误产生的原因
  1. 访问不存在的内存地址
  2. 访问系统保护的内存地址
  3. 访问只读的内存地址
  4. 栈溢出
    详细请参阅:Linux环境下段错误的产生原因及调试方法小结

C++重载实现原理

在这里插入图片描述

C++ 函数调用过程

总结起来整个过程就三步:
1)根据调用的函数名找到函数入口;
2)在栈中审请调用函数中的参数及函数体内定义的变量的内存空间
3)函数执行完后,释放函数在栈中的审请的参数和变量的空间,最后返回值(如果有的话)
详细请查阅:函数调用过程 / C/C++函数调用过程分析

sizeof求类型大小

  1. 类的大小为类的非静态成员数据的类型大小之和,也就是说静态成员数据不作考虑。
  2. 普通成员函数与sizeof无关。
  3. 虚函数由于要维护在虚函数表,所以要占据一个指针大小,也就是4字节。
  4. 类的总大小也遵守类似class字节对齐的,调整规则。
    例如有如下结构体:
struct Stu
{
    int id;
    char sex;
    float hight;
};

那么一个这样的结构体变量占多大内存呢?也就是
cout<<sizeof(Stu)<<endl; 会输出什么?
在了解字节对齐方式之前想当然的会以为:sizeof(Stu) = sizeof(int)+sizeof(char)+sizeof(float) = 9.
然而事实并非如此!

字节对齐原则:在系统默认的对齐方式下:每个成员相对于这个结构体变量地址的偏移量正好是该成员类型所占字节的整数倍,且最终占用字节数为成员类型中最大占用字节数的整数倍。

在这个例子中,id的偏移量为0(0=40),sex的偏移量为4(4=14),hight的偏移量为8(8=24),此时占用12字节,也同时满足12=34.所以sizeof(Stu)=12.

struct A {
	char y;
	char z;
	long long x;
};    16字节
struct A {
	char y;
	char z;
	int x;
};  8字节

struct A {
	char y;
	char* z;
	int x;
};12字节
struct A {
	char y;
};  1字节

在这里插入图片描述
我的总结:

  1. 最终大小一定是最大数据类型的整数倍;
  2. 静态变量不占空间
  3. 每种类型的偏移量为自身的n倍;

详细请查阅:struct/class等内存字节对齐问题详解

如何调试c++ 多线程程序?

  1. 打印日志,日志中加上线程ID;(简单粗暴)
  2. gdb有thread相关命令,如infothread(简写成infoth)显示线程消息,bxxthreadyy可以针对某个thread设置断点,threadxx(简写成thrxx)切换到某个thread。再配合frame(简写f)相关的命令(比如up,down在不同frame间跳转),基本可以处理若干个不同的线程间的debug……

详细请查阅:C++(vs)多线程调试 (转)

面向对象和面向过程的区别

  • 面向对象方法中,把数据和数据操作放在一起,组成对象;对同类的对象抽象出其共性组成类;类通过简单的接口与外界发生联系,对象和对象之间通过消息进行通信。面向对象的三大特性是"封装、“多态”、“继承”,五大原则是"单一职责原则"、“开放封闭原则”、“里氏替换原则”、“依赖倒置原则”、“接口分离原则”
  • 而面向过程方法是以过程为中心的开发方法,它自顶向下顺序进行, 程序结构按照功能划分成若干个基本模块,这些模块形成树状结构

(过程)优点:性能比面向对象高,因为类调用时需要实例化,开销比较大,比较消耗源;比如嵌入式开发、Linux/Unix等一般采用面向过程开发,性能是最重要的因素。缺点:没有面向对象易维护、易复用、易扩展。
(对象)优点:易维护、易复用、易扩展,由于面向对象有封装、继承、多态性的特性,可以设计出低耦合的系统。缺点:性能比面向过程低。

关于引用赋值的多态:

Class B;
Class D : public B;

B& b;
D& d;
B& b1 = d ;  //父类可以作为子类的引用,此时b1表现和指针形式一致(会调用B的非虚函数)
D& d1 = b; //错误,不能将子类作为父类的引用

模板的声明和实现不能分开的原因

  1. 链接的时候,需要实例化模板,这时候就需要找模板的具体实现了。假设在main函数中调用了一个模板函数这时候就需要去实例化该类型的模板。注意main函数里面只包含了.h文件,也就是只有模板的声明,没有具体实现。就会报错
  2. 而模板的实现.cpp里面,虽然有模板的具体实现,但是没有谁在该.cpp里面使用一个模板函数,就不会生成一个具体化的实例

详细请参阅:C++ 模板类的声明与实现分离问题 / C++ 模板类的声明与实现分离问题(模板实例化)

C++类中引用成员和常量成员的初始化(初始化列表)

如果一个类是这样定义的:

Class A
{
     public:
          A(int pram1, int pram2, int pram3);
     privite:
          int a;
          int &b;
          const int c; 
}

假如在构造函数中对三个私有变量进行赋值则通常会这样写:

A::A(int pram1, int pram2, int pram3)
{
     a=pram1;
     b=pram2;
     c=pram3;
}

但是,这样是编译不过的。因为常量和引用初始化必须赋值。所以上面的构造函数的写法只是简单的赋值,并不是初始化。

正确写法应该是:

A::A(int pram1, int pram2, int pram3):b(pram2),c(pram3)
{
     a=pram1;
}

采用初始化列表实现了对常量和引用的初始化。采用括号赋值的方法,括号赋值只能用在变量的初始化而不能用在定义之后的赋值

凡是有引用类型的成员变量或者常量类型的变量的类,不能有缺省构造函数默认构造函数没有对引用成员提供默认的初始化机制,也因此造成引用未初始化的编译错误。并且必须使用初始化列表进行初始化const对象、引用对象。

枚举变量和宏的区别

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

memset为int型数组初始化问题

头文件:#include <string.h>
memset() 函数用来将指定内存的前n个字节设置为特定的值,其原型为:

 void * memset( void * ptr, int value, size_t num );

参数说明:

  • ptr 为要操作的内存的指针。
  • value 为要设置的值。你既可以向 value 传递 int 类型的值,也可以传递 char 类型的值,int 和 char 可以根据 ASCII 码相互转换。
  • num 为 ptr 的前 num 个字节,size_t 就是unsigned int。

【函数说明】memset() 会将 ptr 所指的内存区域的前 num 个字节的值都设置为 value,然后返回指向 ptr 的指针。

无法下面这样初始化,这样的结果是a被赋值成168430090,168430090.。。。。。。。。。

int a[10];
memset(a, 1, sizeof(a));

这是因为int由4个字节(说)表示,并且不能得到数组a中整数的期望值。
但我经常看到程序员使用memset将int数组元素设置为0或-1。其他值不行!

int a[10];
int b[10];
memset(a, 0, sizeof(a));  
memset(b, -1, sizeof(b));
//假设a为int型数组:
memset(a,0x7f,sizeof(a));
//a数组每个空间将被初始化为0x7f7f7f7f,原因是C函数传参过程中的指针降级,导致sizeof(a),返回的是一个 something*指针类型大小的的字节数,如果是32位,就是4字节。所以memset按字节赋值。
memset(a,0xaf,sizeof(a));
//a数组每个空间将被初始化为0xafafafaf

C++中volatile的作用

总结:建议编译器不要对该变量进行优化

volatile是“易变的”、“不稳定”的意思。volatile是C的一个较为少用的关键字,它用来解决变量在“共享”环境下容易出现读取错误的问题。

定义为volatile的变量是说这变量可能会被意想不到地改变,即在你程序运行过程中一直会变,你希望这个值被正确的处理,每次从内存中去读这个值,而不是因编译器优化从缓存的地方读取,比如读取缓存在寄存器中的数值,从而保证volatile变量被正确的读取。

在单任务的环境中,一个函数体内部,如果在两次读取变量的值之间的语句没有对变量的值进行修改,那么编译器就会设法对可执行代码进行优化。由于访问寄存器的速度要快过RAM(从RAM中读取变量的值到寄存器),以后只要变量的值没有改变,就一直从寄存器中读取变量的值,而不对RAM进行访问。

而在多任务环境中,虽然在一个函数体内部,在两次读取变量之间没有对变量的值进行修改,但是该变量仍然有可能被其他的程序(如中断程序、另外的线程等)所修改。如果这时还是从寄存器而不是从RAM中读取,就会出现被修改了的变量值不能得到及时反应的问题。如下程序对这一现象进行了模拟。

#include <iostream>
using namespace std;

int main(int argc,char* argv[])
{
    int i=10;
    int a=i;
    cout<<a<<endl;
    _asm
    {
        mov dword ptr [ebp-4],80
    }
    int b=i;
    cout<<b<<endl;
}
/*
程序在VS2012环境下生成Release版本,输出结果是:
10
10
*/

阅读以上程序,注意以下几个要点:

  • 以上代码必须在Release模式下考查,因为只有Release模式下才会对程序代码进行优化,而这种优化在变量共享的环境下容易引发问题。
  • 在语句b=i;之前,已经通 过内联汇编代码修改了i的值,但是i的变化却没有反映到b中,如果i是一个被多个任务共享的变量,这种优化带来的错误很可能是致命的。
  • 汇编代码[ebp-4]表示变量i的存储单元,因为ebp是扩展基址指针寄存器,存放函数所属栈的栈底地址,先入栈,占用4个字节。随着函数内申明的局部变量的增多,esp(栈顶指针寄存器)就会相应的减小,因为栈的生长方向由高地址向低地址生长。i为第一个变量,栈空间已被ebp入栈占用了4个字节,所以i的地址为ebp-i,[ebp-i]则表示变量i的存储单元。

详细请参考:

https://blog.csdn.net/weixin_41656968/article/details/80958973
https://www.cnblogs.com/god-of-death/p/7852394.html
https://blog.csdn.net/garrulousabyss/article/details/83500576

编译器对 inline 函数的处理步骤

  1. 将 inline 函数体复制到 inline 函数调用点处;
  2. 为所用 inline 函数中的局部变量分配内存空间;
  3. 将 inline 函数的的输入参数和返回值映射到调用方法的局部变量空间中;
  4. 如果 inline 函数有多个返回点,将其转变为 inline 函数代码块末尾的分支(使用 GOTO)
优缺点
  • 优点
  1. 内联函数同宏函数一样将在被调用处进行代码展开,省去了参数压栈、栈帧开辟与回收,结果返回等,从而提高程序运行速度。
  2. 内联函数相比宏函数来说,在代码展开时,会做安全检查或自动类型转换(同普通函数),而宏定义则不会。
  3. 在类中声明同时定义的成员函数,自动转化为内联函数,因此内联函数可以访问类的成员变量,宏定义则不能。
  4. 内联函数在运行时可调试,而宏定义不可以。
  • 缺点
  1. 代码膨胀。内联是以代码膨胀(复制)为代价,消除函数调用带来的开销。如果执行函数体内代码的时间,相比于函数调用的开销较大,那么效率的收获会很少。另一方面,每一处内联函数的调用都要复制代码,将使程序的总代码量增大,消耗更多的内存空间。
  2. inline 函数无法随着函数库升级而升级。inline函数的改变需要重新编译,不像 non-inline 可以直接链接。
  3. 是否内联,程序员不可控。内联函数只是对编译器的建议,是否对函数内联,决定权在于编译器。
虚函数(virtual)可以是内联函数(inline)吗?
  • 虚函数可以是内联函数,内联是可以修饰虚函数的,但是当虚函数表现多态性的时候不能内联。
  • 内联是在编译器建议编译器内联,而虚函数的多态性在运行期,编译器无法知道运行期调用哪个代码,因此虚函数表现为多态性时(运行期)不可以内联。
  • inline virtual 唯一可以内联的时候是:编译器知道所调用的对象是哪个类(如 Base::who()),这只有在编译器具有实际对象而不是对象的指针或引用时才会发生;

Const * 和 *const区别

在这里插入图片描述

自我赋值可能带来的危害有哪些?

  1. “自我赋值安全性”问题:
    考虑一种情况:假如指针x和y同时指向了同一个位于堆内存的对象,此时执行自我赋值时,需要先把一边给删除掉,然后为其重新分配等号右边大小的存储空间。但是此时等号右边和左边指向同一对象已被回收,因此接下来的操作都是在操作已被回收的空间,这都是不安全的。(类把自己内部资源释放掉了,然后又去拷贝这个资源)
  2. “异常安全性”问题:
    在分配新的存储空间时(内存空间不够用,或别的原因)导致的异常安全性问题,此时由于空间没分配成功,左边指向的仍是一个被回收的内存空间。

右值引用

在这里插入图片描述

左值和右值区别:
  • 左值引用, 使用 T&, 只能绑定左值
  • 右值引用, 使用 T&&, 只能绑定右值
  • 常量左值, 使用 const T&, 既可以绑定左值又可以绑定右值
  • 已命名的右值引用,编译器会认为是个左值
  • 编译器有返回值优化,但不要过于依赖

需要注意一下几点:

  1. str6 = std::move(str2),虽然将str2的资源给了str6,但是str2并没有立刻析构,只有在str2离开了自己的作用域的时候才会析构,所以,如果继续使用str2的m_data变量,可能会发生意想不到的错误
  2. 如果我们没有提供移动构造函数,只提供了拷贝构造函数,std::move()会失效但是不会发生错误,因为编译器找不到移动构造函数就去寻找拷贝构造函数,也这是拷贝构造函数的参数是const T&常量左值引用的原因
  3. c++11中的所有容器都实现了move语义,move只是转移了资源的控制权,本质上是将左值强制转化为右值使用,以用于移动拷贝或赋值,避免对含有资源的对象发生无谓的拷贝。move对于拥有如内存、文件句柄等资源的成员的对象有效,如果是一些基本类型,如int和char[10]数组等**,如果使用move,仍会发生拷贝(因为没有对应的移动构造函数)**,所以说move对含有资源的对象说更有意义。
universal references(通用引用)

当右值引用和模板结合的时候,就复杂了。T&&并不一定表示右值引用,它可能是个左值引用又可能是个右值引用。例如:

template<typename T>
void f( T&& param){
    
}
f(10);  //10是右值
int x = 10; //
f(x); //x是左值

如果上面的函数模板表示的是右值引用的话,肯定是不能传递左值的,但是事实却是可以。**这里的&&是一个未定义的引用类型,称为universal references,它必须被初始化,它是左值引用还是右值引用却决于它的初始化,**如果它被一个左值初始化,它就是一个左值引用;如果被一个右值初始化,它就是一个右值引用。

注意:只有当发生自动类型推断时(如函数模板的类型自动推导,或auto关键字),&&才是一个universal references。

完美转发

所谓转发,就是通过一个函数将参数继续转交给另一个函数进行处理,原参数可能是右值,可能是左值,如果还能继续保持参数的原有特征,那么它就是完美的。

void process(int& i){
    cout << "process(int&):" << i << endl;
}
void process(int&& i){
    cout << "process(int&&):" << i << endl;
}

void myforward(int&& i){
    cout << "myforward(int&&):" << i << endl;
    process(i);
}

int main()
{
    int a = 0;
    process(a); //a被视为左值 process(int&):0
    process(1); //1被视为右值 process(int&&):1
    process(move(a)); //强制将a由左值改为右值 process(int&&):0
    myforward(2);  //右值经过forward函数转交给process函数,却称为了一个左值,
    //原因是该右值有了名字  所以是 process(int&):2
    myforward(move(a));  // 同上,在转发的时候右值变成了左值  process(int&):0
    // forward(a) // 错误用法,右值引用不接受左值
}

上面的例子就是不完美转发,而c++中提供了一个std::forward()模板函数解决这个问题。将上面的myforward()函数简单改写一下:

void myforward(int&& i){
    cout << "myforward(int&&):" << i << endl;
    process(std::forward<int>(i));
}
myforward(2); // process(int&&):2

上面修改过后还是不完美转发,myforward()函数能够将右值转发过去,但是并不能够转发左值,解决办法就是借助universal references通用引用类型和std::forward()模板函数共同实现完美转发。例子如下:

#include <iostream>
#include <cstring>
#include <vector>
using namespace std;

void RunCode(int &&m) {
    cout << "rvalue ref" << endl;
}
void RunCode(int &m) {
    cout << "lvalue ref" << endl;
}
void RunCode(const int &&m) {
    cout << "const rvalue ref" << endl;
}
void RunCode(const int &m) {
    cout << "const lvalue ref" << endl;
}
// 这里利用了universal references,如果写T&,就不支持传入右值,而写T&&,既能支持左值,又能支持右值
template<typename T>
void perfectForward(T && t) {
    RunCode(forward<T> (t));
}

template<typename T>
void notPerfectForward(T && t) {
    RunCode(t);
}

int main()
{
    int a = 0;
    int b = 0;
    const int c = 0;
    const int d = 0;

    notPerfectForward(a); // lvalue ref
    notPerfectForward(move(b)); // lvalue ref
    notPerfectForward(c); // const lvalue ref
    notPerfectForward(move(d)); // const lvalue ref

    cout << endl;
    perfectForward(a); // lvalue ref
    perfectForward(move(b)); // rvalue ref
    perfectForward(c); // const lvalue ref
    perfectForward(move(d)); // const rvalue ref
}

上面的代码测试结果表明,在universal references和std::forward的合作下,能够完美的转发这4种类型。

详细请查阅:C++:浅谈右值引用 / 左值和右值区别 / 我理解的右值引用、移动语义和完美转发

总结
  • 由两种值类型,左值和右值。
  • 有三种引用类型,左值引用、右值引用和通用引用。左值引用只能绑定左值,右值引用只能绑定右值,通用引用由初始化时绑定的值的类型确定。
  • 左值和右值是独立于他们的类型的,右值引用可能是左值可能是右值,如果这个右值引用已经被命名了,他就是左值。
  • 引用折叠规则:所有的右值引用叠加到右值引用上仍然是一个右值引用,其他引用折叠都为左值引用。当T&&为模板参数时,输入左值,它将变成左值引用,输入右值则变成具名的右值应用。
  • 移动语义可以减少无谓的内存拷贝,要想实现移动语义,需要实现移动构造函数和移动赋值函数。
  • std::move()将一个左值转换成一个右值,强制使用移动拷贝和赋值函数,这个函数本身并没有对这个左值什么特殊操作。
  • std::forward()和universal references通用引用共同实现完美转发。
  • 用empalce_back()替换push_back()增加性能。
  • 80
    点赞
  • 803
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

秋风遗梦

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值