IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> C++知识库 -> C++++ -> 正文阅读

[C++知识库]C++++

目录

C++和Java的区别

语言特性

  • Java语言给开发人员提供了更为简洁的语法;完全面向对象,由于JVM可以安装到任何的操作系统上,所以说它的可移植性强

  • Java语言中没有指针的概念,引入了真正的数组。不同于C++中利用指针实现的“伪数组”,Java引入了真正的数组,同时将容易造成麻烦的指针从语言中去掉,这将有利于防止在C++程序中常见的因为数组操作越界等指针操作而对系统数据进行非法读写带来的不安全问题

  • C++也可以在其他系统运行,但是需要不同的编码(这一点不如Java,只编写一次代码,到处运行),例如对一个数字,在windows下是大端存储,在unix中则为小端存储。Java程序一般都是生成字节码,在JVM里面运行得到结果

  • Java用接口(Interface)技术取代C++程序中的抽象类。接口与抽象类有同样的功能,但是省却了在实现和维护上的复杂性

垃圾回收

  • C++用析构函数回收垃圾,写C和C++程序时一定要注意内存的申请和释放
  • Java语言不使用指针,内存的分配和回收都是自动进行的,程序员无须考虑内存碎片的问题

应用场景

  • Java在桌面程序上不如C++实用,C++可以直接编译成exe文件,指针是c++的优势,可以直接对内存的操作,但同时具有危险性 。(操作内存的确是一项非常危险的事情,一旦指针指向的位置发生错误,或者误删除了内存中某个地址单元存放的重要数据,后果是可想而知的)
  • Java在Web 应用上具有C++ 无可比拟的优势,具有丰富多样的框架
  • 对于底层程序的编程以及控制方面的编程,C++很灵活,因为有句柄的存在

C++和Python的区别

  • Python是一种脚本语言,是解释执行的,而C++是编译语言,是需要编译后在特定平台运行的。
  • python可以很方便的跨平台,但是效率没有C++高。 Python使用缩进来区分不同的代码块,C++使用花括号来区分
  • C++中需要事先定义变量的类型,而Python不需要,Python的基本数据类型只有数字,布尔值,字符串,列表,元组等等
  • Python的库函数比C++的多,调用起来很方便

c和c++区别

  • C是C++的子集,C++继承和扩展了所有C的语法和特性,
  • 设计思想不同:C语言是面向过程的。C+ +是面向对象的。
    (面向对象:面向对象是理解和抽象现实世界的方法和思想,是通过将需求元素转化为对象来处理问题的思想。)
  • c和c动态管理内存的方法不同。 c使用malloc、free函数。 另一方面,c不仅有malloc/free,还有new/delete关键字
  • C++的类是C中没有的,C中的struct可以在C++中等同类来使用,struct和类的差别是,struct的成员默认访问修饰符是public,而类默认是private
  • C++有引用,但C没有,C仅支持指针
  • C++的高级特性大都是建立在降低效率的基础上实现的,C能够以最简便的方式编译,处理低级存储器,因此更适用于偏底层的设计,像嵌入式,单片机这些

cout和printf有什么区别?

  • cout<<是一个函数,cout<<后可以跟不同的类型是因为cout<<已存在针对各种类型数据的重载,所以会自动识别数据的类型。输出过程会首先将输出字符放入缓冲区,然后输出到屏幕。
    cout是有缓冲输出:
cout < < "abc " < <endl;
或cout < < "abc\n ";cout < <flush; 这两个才是一样的.

flush立即强迫缓冲输出。 printf是无缓冲输出。有输出时立即输出

程序在执行int main(int argc, char *argv[])时的内存结构,你了解吗?

参数的含义是程序在命令行下运行的时候,需要输入argc 个参数,每个参数是以char 类型输入的,依次存在数组里面,数组是 argv[],所有的参数在指针char * 指向的内存中,数组的中元素的个数为 argc 个,第一个参数为程序的名称。

C++三大特性

封装
隐藏对象的属性和实现细节,仅仅对外提供接口和方法。
(将公共的数据或方法使用public修饰,而不希望被访问的数据或方法采用private修饰)
优点: 1)隔离变化;2)便于使用; 3)提高重用性; 4)提高安全性
缺点: 1)如果封装太多,影响效率; 2)使用者不能知道代码具体实现。

继承:

保持原有类特性的基础上进行扩展,派生类从基类获取方法(函数)和属性(成员变量)的过程。(如果类 B 继承于类 A,那么 B 就拥有 A 的方法和属性)
友元关系不能继承,也就是说基类友元不能访问子类私有成员和保护成员

实现方式有两类:

实现继承: 实现继承是指直接使?基类的属性和?法??需额外编码的能?。

接?继承:接?继承是指仅使?属性和?法的名称、但是?类必需提供实现的能?。

例子:将人定义为一个抽象类,拥有姓名、性别、年龄等公共属性,吃饭、睡觉、走路等公共方法,在定义一个具体的人时,就可以继承这个抽象类,既保留了公共属性和方法,也可以在此基础上扩展跳舞、唱歌等特有方法。

多态:

接口的多种不同实现方式即为多态。(同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果)(允许将子类类型的指针赋值给父类类型的指针)

例子
比如电脑的USB接口,既可以插优盘,又可以插鼠标,USB接口就类似类的接口。

静态多态:
函数重载,运算符重载;函数模板
(编译期间就可以确定函数的调?地址,并产?代码)
动态动态
动多态在C++中是通过虚函数实现的 ,即在基类中存在一些接口(一般为纯虚函数),子类必须重载这些接口。这样通过使用基类的指针或者引用指向子类的对象,就可以实现调用子类对应的函数的功能。动多态的函数调用机制是执行期才能进行确定,所以它是动态的。

优点: 1)大大提高了代码的可复用性;2)提高了了代码的可维护性,可扩充性;
缺点: 1)易读性比较不好,调试比较困难 ;2)模板只能定义在.h文件中,当工程大了之后,编译时间十分的变态

C++的异常处理的方法

try、throw和catch关键字
C++中的异常处理机制主要使用try、throw和catch三个关键字,其在程序中的用法


#include <iostream>
using namespace std;
int main()
{
	double m = 1, n = 0;
	try {
		cout << "before dividing." << endl;
		if (n == 0)
			throw - 1; //抛出int型异常
		else if (m == 0)
			throw - 1.0; //拋出 double 型异常
		else
			cout << m / n << endl;
		cout << "after dividing." << endl;
	}
	catch (double d) {
		cout << "catch (double)" << d << endl;
	}
	catch (...) {
		cout << "catch (...)" << endl;
	}
	cout << "finished" << endl;
	return 0;
}
//运行结果
//before dividing.
//catch (...)
//finished

形参与实参的区别

  1. 形参变量只有在被调用时才分配内存单元,在调用结束时, 即刻释放所分配的内存单元。因此,形参只
    有在函数内部有效。 函数调用结束返回主调函数 后则不能再使用该形参变量。
  2. 实参可以是常量、变量、表达式、函数等, 无论实参是何种类型的量,在进行函数调用时,它们都必须
    具有确定的值, 以便把这些值传送给形参。 因此应预先用赋值,输入等办法使实参获得确定值,会产生一
    个临时变量。
  3. 实参和形参在数量上,类型上,顺序上应严格一致, 否则会发生“类型不匹配”的错误。
  4. 函数调用中发生的数据传送是单向的。 即只能把实参的值传送给形参,而不能把形参的值反向地传送给
    实参。 因此在函数调用过程中,形参的值发生改变,而实参中的值不会变化。
  5. 当形参和实参不是指针类型时,在该函数运行时,形参和实参是不同的变量,他们在内存中位于不同的
    位置,形参将实参的内容复制一份,在该函数运行结束的时候形参被释放,而实参内容不会改变。

静态类型和动态类型,静态绑定和动态绑定的介绍

  • 静态类型:对象在声明时采用的类型,在编译期既已确定;
  • 动态类型:通常是指一个指针或引用目前所指对象的类型,是在运行期决定的;
  • 静态绑定:绑定的是静态类型,所对应的函数或属性依赖于对象的静态类型,发生在编译期;
  • 动态绑定:绑定的是动态类型,所对应的函数或属性依赖于对象的动态类型,发生在运行期;

静态函数和虚函数的区别

静态函数是在编译时就已经确定好了运行的时机,而虚函数是使用动态绑定,虚函数使用了虚函数表机制,调用会增加一次的内存开销。

虚函数表具体怎样实现运行时多态

所谓虚函数表是一个类的虚函数地址表,在每个对象创建的时候,都会有一个vptr指向虚函数表,当继承它的子类对虚函数进行重写时,虚函数表中的对应函数地址将被新地址覆盖,所以当父类指针调用子类的成员函数时,虚函数指针就可以指向对应的函数。

当调用虚函数时过程如下(引自More Effective C++):

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

public,protected,private

  • public的变量和函数在类的内部外部都可以访问。
  • protected的变量和函数只能在类的内部和其派生类 中访问。
  • private修饰的元素只能在类内访问。

全局变量和局部变量有什么区别?

  • 生命周期不同:全局变量随主程序创建和创建,随主程序销毁而销毁;局部变量在局部函数内部,甚至局部循环体等内部存在,退出就不存在;
  • 使用方式不同:通过声明后全局变量在程序的各个部分都可以用到;局部变量分配在堆栈区,只能在局部使用。
  • 操作系统和编译器通过内存分配的位置可以区分两者,全局变量分配在全局数据段并且在程序开始运行的时候被加载。局部变量则分配在堆栈里面 。

类的成员函数

(1)拷贝函数
浅拷贝:所谓浅拷贝,指的是在对象复制时,只是对对象中的数据成员进行简单的赋值,默认拷贝构造函数执行的也是浅拷贝。
深拷贝:在“深拷贝”的情况下,对于对象中动态成员,就不能仅仅简单地赋值了,而应该重新动态分配空间。
(2)构造与析构函数
构造函数:主要用来初始化数据。
析构函数:主要用来释放堆区申请的内存空间。

类成员初始化方式?构造函数的执行顺序 ?为什么用成员初始化列表会快一些?

  1. 赋值初始化,通过在函数体内进行赋值初始化;列表初始化,在冒号后使用初始化列表进行初始化。
    这两种方式的主要区别在于:
    对于在函数体中初始化,是在所有的数据成员被分配内存空间后才进行的。
    列表初始化是给数据成员分配内存空间时就进行初始化,就是说分配一个数据成员只要冒号后有此数据成员的赋值表达式(此表达式必须是括号赋值表式),那么分配了内存空间后在进入函数体之前给数据成员赋值,就是说初始化这个数据成员此时函数体还未执行。
  2. 一个派生类构造函数的执行顺序如下:
    ① 虚拟基类的构造函数(多个虚拟基类则按照继承的顺序执行构造函数)。
    ② 基类的构造函数(多个普通基类也按照继承的顺序执行构造函数)。
    ③ 类类型的成员对象的构造函数(按照初始化顺序)
    ④ 派生类自己的构造函数。
  3. 方法一是在构造函数当中做赋值的操作,而方法二是做纯粹的初始化操作。我们都知道,C++的赋值操作是会产生临时对象的。临时对象的出现会降低程序的效率。

当C++定义类时,编译器会为类自动生成哪些函数?这些函数各自都有什么特点?

  1. 默认构造函数
  2. 默认析构函数
  3. 拷贝构造函数
  4. 默认赋值函数
  5. 两个取址运算符
class Empty
{
  public:
    Empty();                            //缺省构造函数
    Empty(const Empty &rhs);            //拷贝构造函数
    ~Empty();                           //析构函数 
    Empty& operator=(const Empty &rhs); //赋值运算符
    Empty* operator&();                 //取址运算符
    const Empty* operator&() const;     //取址运算符(const版本)
};
调用
Empty *e = new Empty();    //缺省构造函数
delete e;                  //析构函数
Empty e1;                  //缺省构造函数                               
Empty e2(e1);              //拷贝构造函数
e2 = e1;                   //赋值运算符
Empty *pe1 = &e1;          //取址运算符(非const)
const Empty *pe2 = &e2;    //取址运算符(const)

注意: 唯有当这些函数被需要(被调用)时,它们才会被编译器创建出来。但是这些函数一般都会被使用到。
如果自定义了如上的函数,则编译器不会生成对应的默认的函数。

什么是虚函数,为什么析构函数必须是虚函数,为什么c++默认析构函数不是虚函数

虚函数是在某个基类中声明,在其派生类中被重写的成员函数。用于实现多态性,简单来说就是,对于不同的类,相同的方法可以采用不同的策略。

如果析构函数不是虚函数,那么当一个派生类经由一个基类指针删除的时候,其结果是未定义的,实际实行的时候,通常是对象的派生类部分没有被销毁,而其中基类部分被销毁掉了,就产生了一种局部销毁的现象, 从而造成资源泄漏。

为了消除这个问题,就必须在基类中定义virtual的析构函数,从而销毁对象时,才能完整销毁。

将可能会被继承的父类的析构函数设置为虚函数,可以保证当我们new一个子类,然后使用基类指针指向该子类对象,释放基类指针时可以释放掉子类的空间,防止内存泄漏。

C++默认的析构函数不是虚函数是因为虚函数需要额外的虚函数表和虚表指针,占用额外的内存。而对于不会被继承的类来说,其析构函数如果是虚函数,就会浪费内存。因此C++默认的析构函数不是虚函数,而是只有当需要当作父类时,设置为虚函数。

如何让一个类不能实例化?

将类定义为抽象基类或者将构造函数声明为private

staic(修饰的数据成员存储在全局数据区)

(1)隐藏。 修饰全局变量时,表明一个全局变量只对定义在同一文件中的函数可见。 (静态函数/静态全局变量不能被其他文件所用;其他文件可以定义相同名字的函数,不会发生冲突)
(2)修饰局部变量时,表明该变量的值不会因为函数终止而丢失。
(保持变量内容的持久)存储在静态数据区的变量会在程序刚开始运行时就完成初始化,也是唯一的一次初始化。共有两种变量存储在静态存储区:全局变量和 static 变量。
(3)默认初始化为 0.(其实全局变量也具备这一属性,因为全局变量也存储在静态数据区。在静态数据区,内存中所有的字节默认值都是 0×00,某些时候这一特点可以减少程序员的工作量。)
(4)类的普通成员函数要通过对象调用,所以要求首先建立一个对象(实例化之后才有,在动态存储区,堆);而静态成员函数可不建立对象就可以被使用(实例化之前就有,静态)

C++如何创建一个类,使得他只能在堆或者栈上创建?

  • 只能在堆上生成对象:将析构函数设置为私有
    原因:C++是静态绑定语言,编译器管理栈上对象的生命周期,编译器在为类对象分配栈空间时,会先检查类的析构函数的访问性。若析构函数不可访问,则不能在栈上创建对象。
  • 只能在栈上生成对象:将new 和 delete 重载为私有
    原因:在堆上生成对象,使用new关键词操作,其过程分为两阶段:第一阶段,使用new在堆上寻找可用内存,分配给对象;第二阶段,调用构造函数生成对象。将new操作设置为私有,那么第一阶段就无法完成,就不能够再堆上生成对象。

析构函数能否抛出异常

不能

  1. 如果析构函数抛出异常,则异常点之后的程序不会执行,如果析构函数在异常点之后执行了某些必要的动作比如释放某些资源,则这些动作不会执行,会造成诸如资源泄漏的问题。
  2. 通常异常发生时,c++的异常处理机制在异常的传播过程中会进行栈展开(stack-unwinding)在栈展开的过程中 会释放局部对象所占用的内存并运行类类型局部对象的析构函数 ,此时若其他析构函数本身也抛出异常,则前一个异常尚未处理,又有新的异常,会造成程序崩溃。

解决方法

  • 直接结束程序
  • 把可能产生异常的代码移出析构函数
  • 直接消化处理异常

内联函数 inline

(1)目的:为了解决程序中函数调用的效率问题
(2)具体:程序在编译器编译的时候,编译器将程序中出现的内联函数的调用表达式用内联函数的函数体进行替换,而对于其他的函数,都是在运行时候才被替代。
(3)这其实就是个空间代价换时间的节省。所以内联函数一般都是1-5行的小函数。
(限制)在使用内联函数时要留神:1.在内联函数内不允许使用循环语句和开关语句;2.内联函数的定义必须出现在内联函数第一次调用之前;3.类结构中所在的类说明内部定义的函数是内联函数。
(4)和宏定义#define的区别

  1. 宏定义不是函数,但是使用起来像函数。预处理器用复制宏代码的方式代替函数的调用,省去了函数压栈退栈过程,提高了效率。

    内联函数本质上是一个函数,内联函数一般用于函数体的代码比较简单的函数,不能包含复杂的控制语句,while、switch,并且内联函数本身不能直接调用自身。如果内联函数的函数体过大,编译器会自动的把这个内联函数变成普通函数。

  2. 宏定义是在预编译的时候把所有的宏名用宏体来替换,简单的说就是字符串替换

    内联函数则是在编译的时候进行代码插入,编译器会在每处调用内联函数的地方直接把内联函数的内容展开,这样可以省去函数的调用的开销,提高
    效率

  3. 宏定义是没有类型检查的,无论对还是错都是直接替换内联函数在编译的时候会进行类型的检查,内联函数满足函数的性质,比如有返回值、参数列表等

  4. 宏定义和内联函数使用的时候都是进行代码展开。不同的是宏定义是在预编译的时候把所有的宏名替换,内联函数则是在编译阶段把所有调用内联函数的地方把内联函数插入。这样可以省去函数压栈退栈,提高了效率

inline跨文件使用

内联函数必须在调用它的每个文件中定义, 若想在所有文件中使用,最好在头文件中定义,且一旦内联函数在多个头文件中定义,则会产生内联函数的重定义

常量指针和指针常量

【代码】

常量指针:指向常量的指针,(地址可变,内容不变),也是底层const(对象(指针、引用等)指向的是一个常量)

int const *p1 = &b;*//const 在前,定义为常量指针*

指针常量:表示指针是一个常量,(地址不变,内容可变)无法改变其指向的内存空间。顶层const(指对象本身就是一个常量)

int *const p2 = &c;*// *在前,定义为指针常量*

左值和右值

左右值引用

左值是对应内存中有确定存储地址的对象的表达式的值
右值是所有不是左值的表达式的值
能进行取址符操作的都是左值,其他的都是右值
(表达式:由一个或多个操作数通过操作符组合而成。最简单的表达式仅包含一个字面值常量或变量。较复杂的表达式则由操作符以及一个或多个操作数构成。)

std::move

  1. std::move的本质就强制类型转换,它无条件地将实参转为右值引用类型

  2. std::move 是将对象的状态或者所有权从一个对象转移到另一个对象,只是转移,没有内存的搬迁或者内存拷贝

指针和引用

(1)引用必须被初始化(引用类型的初始值必须是一个对象),指针不必(但最好要初始化)。

(2)引用无法更改为另一个对象的引用,而指针可以任意改变指向的对象。

(3)不存在指向空值的引用,但是存在指向空值的指针。

(4)指针通过某个指针变量指向一个对象后,对它所指向的变量间接操作。引用本身就是目标变量的别名,对引用的操作就是对目标变量的操作。

(5)指针是一个对象,可以定义指向指针的指针。但引用不是对象,没有实际地址,所以不能定义指向引用的指针,也不能定义指向引用的引用。
(6)程序不给引用分配内存, 而程序会为指针分配4个字节的内存.

(7)对与,sizeof取大小,指针固定为4,引用是对象的大小

相同点:
(1)都是地址的概念;

(2)指针指向一块内存,它的内容是所指内存的地址;

(3)引用是某块内存的别名。

普通指针

(1)忘记释放资源,导致资源泄露(常发生内存泄漏问题)
(2)同一资源释放多次,导致释放野指针,程序崩溃
(3)明明代码的后面写了释放资源的代码,但是由于程序逻辑满足条件,从中间return掉了,导致释放资源的代码未被执行到
(4)代码运行过程中发生异常,随着异常栈展开,导致释放资源的代码未被执行到

野指针和悬空指针

都是是指向无效内存区域(这里的无效指的是"不安全不可控")的指针,访问行为将会导致未定义行为。

野指针 : 指的是没有被初始化过的指针

int main(void) { 
    int* p; // 未初始化 
    std::cout<< *p << std::endl; // 未初始化就被使用 
    return 0; 
}
因此,为了防止出错,对于指针初始化时都是赋值为 nullptr ,这样在使用时编译器就会直接报错,产生非法内存访问。

悬空指针: 指针最初指向的内存已经被释放了的一种指针。

int main(void) { 
    int * p = nullptr; 
    int* p2 = new int;
    p = p2; 
    delete p2; 
}

此时 p和p2就是悬空指针,指向的内存已经被释放。继续使用这两个指针,行为不可预料。需要设置为p=p2=nullptr 。此时再使用,编译器会直接保错。 避免野指针比较简单,但悬空指针比较麻烦。c++引入了智能指针,C++智能指针的本质就是避免悬空指针的产生。

产生原因及解决办法:

**野指针:**指针变量未及时初始化 => 定义指针变量及时初始化,要么置空。
悬空指针: 指针free或delete之后没有及时置空 => 释放操作后立即置空

c++的三种智能指针

c++11标准引入了三种智能指针,均定义在头文件下,其本质上是一个模板类,拥有构造函数和析构函数,在超出其作用域后,它会主动释放其管理的内存。
主要体现在用户可以不关注资源的释放,因为智能指针会帮你完全管理资源的释放,它会保证无论程序逻辑怎么跑,正常执行或者产生异常,资源在到期的情况下,一定会进行释放。
具体:
1)智能指针体现在把裸指针进行了一次面向对象的封装,在构造函数中初始化资源地址,在析构函数中负责释放资源
2)利用栈上的对象出作用域自动析构这个特点,在智能指针的析构函数中保证释放资源
shared_ptr

shared_ptr是一种共享式指针,采用引用计数,指针之间共享内存,传递一次引用就加1,引用数为0时自动销毁内存。shared_ptr可以使用方法use_count()查看拥有管理权的shared_ptr的个数,可以使用release()方法释放其所有权,此时引用计数-1。

unique_ptr

为了保证同一时间仅能有一个指针来管理一块内存,c++11引入了unique_ptr,它是一种独占式指针,仅支持一个指针指向该内存。可以使用std::move来移动unique_ptr指向的内存。

weak_ptr

当有两个shared_ptr相互指向发生循环引用时,会产生死锁导致内存泄漏,因此c++11为了防止死锁现象的发生,引入了弱引用的概念,它的存在不会改变内存的引用计数,仅仅用于辅助shared_ptr来管理内存,提供一个访问内存的方式,可用于核查指针类,即检查该对象是否已经被释放。

函数指针和指针函数

函数指针

指向函数的指针变量。函数指针本身首先是一个指针变量,该指针变量指向一个具体的函数, 这正如用指针变量可指向整形变量, 字符型, 数组一样,这里是指向函数。

用途:
调用函数和做函数的参数, 比如回调函数

int (*f)(int a, int b); // 声明函数指针

指针函数

一个返回指针的函数,其本质是一个函数,而该函数的返回值是一个指针。
声明格式为:*类型标识符 函数名(参数表)

int *ans(int x, int y) {}

C++中关于变量的存储位置

BSS段 :通常是指用来存放程序中 未初始化的全局变量、静态变量(全局变量未初始化时默认为0)的一块内存区域

数据段 :通常是指用来存放程序中 初始化后的全局变量和静态变量

代码段 :通常是指用来存放程序中 代码和常量

堆 :通常是指用来存放程序中 进程运行时被动态分配的内存段 ( 动态分配:malloc / new,者动态释放:free / delete)

栈 :通常是指用来存放程序中 用户临时创建的局部变量、函数形参、数组(局部变量未初始化则默认为垃圾值)也就是说我们函数括弧“{}”中定义的变量(但不包括static声明的变量,static意味着在数据段中存放变量)

堆(heap)和栈(stack)的区别

如何让一个函数在main函数执行前先执行

C++的四种强制转换reinterpret_cast/const_cast/static_cast /dynamic_cast

隐式类型转换

概念:
c++自动将一种类型转换成另一种类型,是编译器的一种自主行为

对于内置类型,当运算符两端类型不同时,编译器会自动使低精度的类型向高精度类型转换。

在类的构造函数中,可以直接传入参数,编译器会为其生成一个临时对象用于构造。
explicit关键字禁止编译器进行隐式类型转换。

new/delete与malloc/free的区别

new和delete是c++的关键字,而malloc和free是内置函数,当使用new对对象进行分配空间时,编译器会自动得到该对象的大小,但是malloc需要显式给出需要分配的空间大小

对于自定义的类来说,new会先调用operator new申请足够的空间,然后调用类类型的构造函数,最后返回该类型的指针,delete会先调用类的析构函数,然后调用operator delete释放内存空间。而malloc和free是内置函数,无法要求他们调用构造函数和析构函数。

内存分配方式

内存分配方式有三种:

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

[2]在栈上创建。在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理
器的指令集中,效率很高,但是分配的内存容量有限。

[3]从堆上分配,亦称动态内存分配。程序在运行的时候用malloc或new申请任意多少的内存,程序员自己负责在何时用free或delete释放内存。动态内存的生存期由程序员决定,使用非常灵活,但如果在堆上分配了空间,就有责任回收它,否则运行的程序会出现内存泄漏,频繁地分配和释放不同大小的堆空间将会产生堆内碎块。

c++内存管理

c++中内存分为五个区域

  • 堆区, 程序需要主动申请分配,主动释放的区域。可以使用malloc申请
  • 栈区, 当创建对象时由程序主动分配
  • 全局变量区, 创建的静态变量或全局变量
  • 文字常量区, 字面值常量以及字符常量,比如printf中的格式化输出字符
  • 代码区,代码区段

C如何进行函数调用

对于每个函数,c++都会为它分配一个栈,在进行函数调用之前,先将当前指令的esp指针压入栈中,并将参数入栈,跳转到函数存储的地址,函数执行结束后,恢复esp指针,回到原地址继续运行。

拷贝构造函数的调用时机

1.直接初始化和拷贝初始化

2.将一个对象作为实参传递给一个非引用或非指针类型的形参时

3.从一个返回类型为非引用或非指针的函数返回一个对象时

4.用花括号列表初始化一个数组的元素或者一个聚合类(很少使用)中的成员时。

STL由什么组成

容器:容纳,包含一种元素或元素集合。

迭代器: 用于遍历,访问容器中的元素,一般作为泛型算法的参数。

仿函数:

泛型算法:用来操作容器中元素的方法。

分配器:为容器等分配空间。

配接器:将一个class的接口转换为另一个class的接口,使原本因接口不兼容不能合作的两个class共同运作。

map 和set的区别,它们是如何实现的

map和set都是c++的关联容器,其底层实现都是红黑树,他们所有的接口都由红黑树给出,所以几乎所有的操作行为都是转调红黑树的操作。

区别:

map是映射,其中的元素是key-value的,可以按key值来索引value值。

set是集合,其中元素只是一个值,仅包含一个关键字。

set的迭代器是const的,它不支持使用迭代器修改元素,而map允许修改value的值,他们的元素都是根据关键字来保证有序的,所以不能轻易修改,只能将原关键字删除,重新插入,但是对于这些操作都是O(logn)的,所以时间开销较大。

另外,map支持下标操作,set不支持。

vector 和list的区别,它们是如何实现的

vector和list都是c++中的容器,vector是向量,底层存储空间是连续的,也就是数组,所以对于随机读取修改所需时间较低,为O(1),但插入的复杂度较低,每次插入要将其后面的元素向后移动,所以最坏时间复杂度是O(n)的。且在可分配空间不足时,可能需要将所有的数据移动到另一块内存。

list底层实现是双向链表,底层存储时非连续的,随机读取只能从头节点向后查找,所以最坏时间复杂度是O(n)的,但插入仅需 O(1)。

有哪些内存泄漏?如何判断内存泄漏?如何定位内存泄漏?

内存泄漏是指堆内存的泄露,堆内存在程序中由程序动态分配的内存,使用过后需要显示释放,有些时候忘记释放已使用完的内存,就会发生内存泄漏。若运行过久,可能会导致栈溢出致使程序崩溃。
避免内存泄露的几种方式

  • 计数法:使用new或者malloc时,让该数+1,delete或free时,该数-1,程序执行完打印这个计数,如果不为0则表示存在内存泄露
  • 一定要将基类的析构函数声明为虚函数
  • 对象数组的释放一定要用delete []
  • 有new就有delete,有malloc就有free,保证它们一定成对出现

检测工具

  • Linux下可以使用Valgrind工具
  • Windows下可以使用CRT库
    (c++无法检测内存泄漏,但是可以依靠top命令查看进程的动态内存总额,也可以使用mtrace来检)

动态链接和静态链接的区别

  • 静态链接

    所谓静态链接就是在编译链接时直接将代码拷贝至链接处,他的优点是可以独立于库进行发布,但是若静态库文件过大,容易造成资源的浪费。

  • 动态链接

    动态链接就是编译的时候不将代码拷贝到文件,而是只复制了一些重定位信息和符号表信息,在程序运行或加载时,将这些信息传递给操作系统,操作系统负责将动态库加载到内存中,程序运行到指定代码时,再去执行已经加载到内存中的函数。

  • 静态链接存在着明显的缺点,一是资源的浪费,对于多个可执行文件均调用同一个模块时,需要将每个模块都要拷贝到内存中。二是当静态库文件过大时,若想更新静态库存在着诸多不便。而动态库就是为了解决这两个问题而生。

  • 动态链接将程序按模块拆分,在构建可执行程序时,发现该函数十几个外部符号,则将其放到运行时进行处理,运行时对其进行重定位。动态链接相比静态链接更加灵活,解决了模块拷贝到内存时的资源浪费,虽然性能相对存在一定的下降,但是相比灵活性的提升,显然是更值得的。测定位内存泄漏。

allocator和new区别

allocator:
allocator是STL的六大组件之一,空间配置器,为各个容器管理内存(内存开辟 内存回收)

new分配的空间会造成空间的浪费,因为new分配的空间将内存分配和对象构造组合在一起,造成不必要的浪费。
标准库中将allocator类定义在头文件memory中,它可以实现内存分配和对象指定的分离,(allocator使用模板定义实现的,因此使用时需要指定数据类型)

一个allocator类调用allocate来分配空间,调用construct来构造对象,destroy来销毁对象。

STL迭代器删除元素

对于顺序容器,当使用erase删除元素后,会导致排在后面的迭代器失效,每个元素向前移动一位,但是erase会指向下一个迭代器。

对于关联容器,由于底层总是树形结构或者哈希结构,对后面的迭代器是没有影响的。
如何解决

resize和reserve的区别

resize是对改变当前容器元素的数量,若resize大小小于当前容器元素的数量,则删除后面的元素。

reserve是改变其预留空间,保证内存空间可容纳的元素数量,并不生成新的对象,若参数小于当前容器元素数量,则不改变当前的元素数量。

c++内存对齐

内存对齐就是计算机系统对数据存放位置的限制。内存是以字节为单位,但是大部分处理器并不是按字节块来存取内存的.它一般会以双字节,四字节,8字节,16字节甚至32字节为单位来存取内存,我们将上述这些存取单位称为内存存取粒度

大部分处理器的内存存取粒度是4字节8字节这样的,如果不进行内存对齐,可能会产生一个整数的存储位置被分到两块内存,需要cpu进行两次读取再拼接,需要做的工作十分复杂,而对齐后可以将一个数据一次直接读取出来,提高cpu的读取效率,一般编译器默认的内存对齐系数是4,在结构体或类中,内存对齐系数一般为其成员变量的最大内存对齐系数。

可以使用#pragma pack(n)来改变默认对齐系数

如何获得结构成员相对于结构开头的字节偏移量

结构体的对齐,为什么要对齐

内存对齐主要遵从以下三个原则

  1. 结构体变量的起始地址能够被其最宽的成员大小整除
  2. 结构体每个成员相对于起始地址的偏移能够被其自身大小整除,如果不能则在前一个成员后面补充字节
  3. 结构体总体大小能够被最宽的成员的大小整除,如不能则在后面补充字节

为什么需要对齐?

1.平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
2.性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问

说一下c++的编译过程

  1. 编译预处理
    预处理阶段主要是做一些代码替换的工作,处理预处理指令,解包头文件,替换宏定义,删掉注释等等。

  2. 编译、优化阶段

    通过词法语法分析,将代码文件转换为汇编代码

  3. 汇编过程

    将汇编代码转换为指定目标的机器指令,以便在目标机器上运行。

  4. 链接程序

    链接程序就是将代码所用到的模块,代码片段等连接起来,使其可以运行。

深拷贝与浅拷贝

  • 深拷贝是直接将内存拷贝出一份

深拷贝不仅拷贝值,还开辟出一块新的空间用来存放新的值,即使原先的对象被析构掉,释放内存了也不会影响到深拷贝得到的值。在自己实现拷贝赋值的时候,如果有指针变量的话是需要自己实现深拷贝的

  • 浅拷贝: 只是将指针拷贝指向同一块内存

浅拷贝只是拷贝一个指针,并没有新开辟一个地址,拷贝的指针和原来的指针指向同一块地址,如果原来的指针所指向的资源释放了,那么再释放浅拷贝的指针的资源就会出现错误。

当类成员存在指针时,若使用默认构造函数使用简单的浅拷贝,那么当使用析构函数释放资源时,会提前释放成员指针指向的数据,可能造成空悬指针多次释放导致内存泄漏。

零拷贝

  • 零拷贝就是一种避免 CPU 将数据从一块存储拷贝到另外一块存储的技术。

  • 零拷贝技术可以减少数据拷贝和共享总线操作的次数。

  • 在C++中,vector的一个成员函数emplace_back()很好地体现了零拷贝技术,它跟push_back()函数一样
    可以将一个元素插入容器尾部,区别在于:使用push_back()函数需要调用拷贝构造函数和转移构造函数,而使用emplace_back()插入的元素原地构造,不需要触发拷贝构造和转移构造,效率更高

#include <vector>
#include <string>
#include <iostream>
using namespace std;
struct Person
{
	string name;
	int age;
	//初始构造函数
	Person(string p_name, int p_age) : name(std::move(p_name)), age(p_age)
	{
		cout << "I have been constructed" << endl;
	}
	//拷贝构造函数
	Person(const Person& other) : name(std::move(other.name)), age(other.age)
	{
		cout << "I have been copy constructed" << endl;
	}
	//转移构造函数
	Person(Person&& other) : name(std::move(other.name)), age(other.age)
	{
		cout << "I have been moved" << endl;
	}
};
int main()
{
	vector<Person> e;
	cout << "emplace_back:" << endl;
	e.emplace_back("Jane", 23); //不用构造类对象
	vector<Person> p;
	cout << "push_back:" << endl;
	p.push_back(Person("Mike", 36));
	return 0;
}
//输出结果:
//emplace_back:
//I have been constructed
//push_back:
//I have been constructed
//I am being moved.

工作原理

系统调用直接通过DMA将数据拷贝到内核缓冲区,然后被内核直接转发到与另一个文件相关的内核缓冲区,其中一直都是内核态,不需要进入用户态。

线程安全

线程安全是指内存安全,也就是保证本线程所使用的数据,不被其他线程暗改,导致得到的数据不是自己想要的数据。

解决线程安全的办法

  1. 使用线程自己的栈内存,每个线程都存在自己所独有的栈,其他线程无法影响,可以把需要保证安全的数据存放在自己的栈区中,这样其他线程无法访问,也就保证了线程安全
  2. 对于所需数据copy一份自己的,每个线程都copy一份原数据,每个线程只访问属于自己的那部分数据,也就保证了线程安全
  3. 定义常变量,也就是只读变量,保证变量无法被修改。
  4. 使用互斥锁,保证内存中的数据互斥访问。

c++ 11/14/17新特性

11:

  • 智能指针

  • override关键字

  • =delete/=default

  • auto

  • for each语法

  • 无序容器

  • nullptr

  • lambda匿名函数

  • 右值引用 和 移动语义

  • explicit/override/final/noexcept

  • std::function

14:

  • 泛型lambda
  • 二进制字面值
  • 数字分隔符

17:

  • 扩展了auto的推断范围
  • 嵌套命名空间
  • 条件分支语句初始化

什么是回调函数?为什么要使用回调函数?如何使用回调函数?

  C++知识库 最新文章
【C++】友元、嵌套类、异常、RTTI、类型转换
通讯录的思路与实现(C语言)
C++PrimerPlus 第七章 函数-C++的编程模块(
Problem C: 算法9-9~9-12:平衡二叉树的基本
MSVC C++ UTF-8编程
C++进阶 多态原理
简单string类c++实现
我的年度总结
【C语言】以深厚地基筑伟岸高楼-基础篇(六
c语言常见错误合集
上一篇文章      下一篇文章      查看所有文章
加:2022-04-28 11:37:29  更:2022-04-28 11:39:35 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/21 0:55:46-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码