C++面试题

参考文献1

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

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

1).预处理,产生.ii文件

2).编译,产生汇编文件(.s文件)

3).汇编,产生目标文件(.o或.obj文件)

4).链接,产生可执行文件(.out或.exe文件)

进程和线程,为什么要有线程

1)和进程相比,它是一种非常"节俭"的多任务操作方式。在linux系统下,启动一个新的进程必须分配给它独立的地址空间,建立众多的数据表来维护它的代码段、堆栈段和数据段,这是一种"昂贵"的多任务工作方式。线程是不需要分配资源的,多个线程共用其进程的资源。(资源)

2)运行于一个进程中的多个线程,它们之间使用相同的地址空间,而且线程间彼此切换所需时间也远远小于进程间切换所需要的时间。据统计,一个进程的开销大约是一个线程开销的30倍左右。(切换效率)

3)线程间方便的通信机制。对不同进程来说,它们具有独立的数据空间,要进行数据的传递只能通过进程间通信的方式进行,这种方式不仅费时,而且很不方便。线程则不然,由于同一进程下的线程之间贡献数据空间,所以一个线程的数据可以直接为其他线程所用,这不仅快捷,而且方便。(通信)

除以上优点外,多线程程序作为一种多任务、并发的工作方式,还有如下优点:
1)使多CPU系统更加有效。操作系统会保证当线程数不大于CPU数目时,不同的线程运行于不同的CPU上。(CPU设计保证)
2)改善程序结构。一个既长又复杂的进程可以考虑分为多个线程,成为几个独立或半独立的运行部分,这样的程序才会利于理解和修改。(代码易维护)

线程

  • 线程概念:
    进程在各自独立的地址空间中运行,进程之间共享数据需要用mmap或者进程间通信机制;一个进程可以有多个线程,各个线程共享该进程的资源且共享地址空间和数据空间。
    每个线程各有一份的

  • 线程id

  • 上下文, 包括各种寄存器的值、程序计数器和栈指针

  • 栈空间

  • errno变量

  • 信号屏蔽字

  • 调度优先

  • 实现多线程
    -创建线程
    成功返回0,失败返回错误号;而pthread库的函数都是通过返回值返回错误号,虽然每个线程也都有一个errno,但这是为了兼容其它函数接口而提供的,pthread库本身并不使用它,通过返回值返回错误码更加清晰。

    在一个线程中调用pthread_create()创建新的线程后,当前线程从pthread_create()返回,然后继续往下执行,而新的线程所执行的代码由我们传给pthread_create的函数指针start_routine决定。

    start_routine函数接收一个参数,是通过pthread_create的arg参数传递给它的,该参数的类型为void *,这个指针按什么类型解释由调用者自己定义。start_routine的返回值类型也是void *,这个指针的含义同样由调用者自己定义。start_routine返回时,这个线程就退出了,其它线程可以调用pthread_join得到start_routine的返回值,类似于父进程调用wait(2)得到子进程的退出状态,稍后详细介绍pthread_join。

    pthread_create成功返回后,新创建的线程的id被填写到thread参数所指向的内存单元。我们知道进程id的类型是pid_t,每个进程的id在整个系统中是唯一的,调用getpid(2)可以获得当前进程的id,是一个正整数值。线程id的类型是thread_t,它只在当前进程中保证是唯一的,在不同的系统中thread_t这个类型有不同的实现,它可能是一个整数值,也可能是一个结构体,也可能是一个地址,所以不能简单地当成整数用printf打印,调用pthread_self(3)可以获得当前线程的id。

    attr参数表示线程属性

小结:

-终止线程
-线程间同步

malloc和free

1)malloc 函数的实质是它有一个将可用的内存块连接为一个长长的列表的所谓空闲链表。

2)调用 malloc()函数时,它沿着连接表寻找一个大到足以满足用户请求所需要的内存块。 然后,将该内存块一分为二(一块的大小与用户申请的大小相等,另一块的大小就是剩下来的字节)。 接下来,将分配给用户的那块内存存储区域传给用户,并将剩下的那块(如果有的话)返回到连接表上。

3)调用 free 函数时,它将用户释放的内存块连接到空闲链表上。

4)到最后,空闲链会被切成很多的小内存片段,如果这时用户申请一个大的内存片段, 那么空闲链表上可能没有可以满足用户要求的片段了。于是,malloc()函数请求延时,并开始在空闲链表上检查各内存片段,对它们进行内存整理,将相邻的小空闲块合并成较大的内存块。

内存分配的原理

从操作系统角度来看,进程分配内存有两种方式,分别由两个系统调用完成:brk和mmap(不考虑共享内存)。

1、brk是将数据段(.data)的最高地址指针_edata往高地址推;

2、mmap是在进程的虚拟地址空间中(堆和栈中间,称为文件映射区域的地方)找一块空闲的虚拟内存。

这两种方式分配的都是虚拟内存,没有分配物理内存。在第一次访问已分配的虚拟地址空间的时候,发生缺页中断,操作系统负责分配物理内存,然后建立虚拟内存和物理内存之间的映射关系。

内存管理方式

在c++中内存主要分为5个存储区:

  • 栈(Stack):局部变量,函数参数等存储在该区,由编译器自动分配和释放.栈属于计算机系统的数据结构,进栈出栈有相应的计算机指令支持,而且分配专门的寄存器存储栈的地址,效率分高,内存空间是连续的,但栈的内存空间有限。
  • 堆(Heap):需要程序员手动分配和释放(new,delete),属于动态分配方式。内存空间几乎没有限制,内存空间不连续,因此会产生内存碎片。操作系统有一个记录空间内存的链表,当收到内存申请时遍历链表,找到第一个空间大于申请空间的堆节点,将该节点分配给程序,并将该节点从链表中删除。一般,系统会在该内存空间的首地址处记录本次分配的内存大小,用于delete释放该内存空间。
  • 全局/静态存储区:全局变量,静态变量分配到该区,到程序结束时自动释放,包括DATA段(全局初始化区)与BBS段(全局未初始化段)。其中,初始化的全局变量和静态变量存放在DATA段,未初始化的全局变量和静态变量存放在BBS段。BBS段特点:在程序执行前BBS段自动清零,所以未初始化的全局变量和静态变量在程序执行前已经成为0.
  • 文字常量区:存放常量,而且不允许修改。程序结束后由系统释放。
  • 程序代码区:存放程序的二进制代码

堆和栈的区别:

1)空间大小:栈的内存空间是连续的,空间大小通常是系统预先规定好的,即栈顶地址和最大空间是确定的;而堆得内存空间是不连续的,由一个记录空间空间的链表负责管理,因此内存空间几乎没有限制,在32位系统下,内存空间大小可达到4G

2)管理方式:栈由编译器自动分配和释放,而堆需要程序员来手动分配和释放,若忘记delete,容易产生内存泄漏。

3)生长方向不同:对于栈,他是向着内存地址减小的方向生长的,这也是为什么栈的内存空间是有限的;而堆是向着内存地址增大的方向生长的

4)碎片问题:由于栈的内存空间是连续的,先进后出的方式保证不会产生零碎的空间;而堆分配方式是每次在空闲链表中遍历到第一个大于申请空间的节点,每次分配的空间大小一般不会正好等于申请的内存大小,频繁的new操作势必会产生大量的空间碎片

5)分配效率:栈属于机器系统提供的数据结构,计算机会在底层对栈提供支持,出栈进栈由专门的指令执行,因此效率较高。而堆是c/c++函数库提供的,当申请空间时需要按照一定的算法搜索足够大小的内存空间,当没有足够的空间时,还需要额外的处理,因此效率较低。

为什么栈相对堆很小

1.大块内存不适合动态【增长/回退】,对于短声明周期对的块尽量动态增长,
2.堆通常是进程内共用的,栈通常是线程私有的,一个进程包含多个线程,就有必要比栈大了
3,栈大小其实也是可以调整的

c++和c的区别:

1)C是面向过程的语言,是一个结构化的语言,考虑如何通过一个过程对输入进行处理得到输出;C++是面向对象的语言,主要特征是“封装、继承和多态”。封装隐藏了实现细节,使得代码模块化;派生类可以继承父类的数据和方法,扩展了已经存在的模块,实现了代码重用;多态则是“一个接口,多种实现”,通过派生类重写父类的虚函数,实现了接口的重用。

2)C和C++动态管理内存的方法不一样,C是使用malloc/free,而C++除此之外还有new/delete关键字。

3)C++支持函数重载,C不支持函数重载

4)C++中有引用,C中不存在引用的概念

extern C 的作用是什么?

指示编译器这段代码按照c语言执行而不是c++语言执行。

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

1)指针是一个新的变量,存储了另一个变量的地址,我们可以通过访问这个地址来修改另一个变量;引用只是一个别名,还是变量本身,对引用的任何操作就是对变量本身进行操作,以达到修改变量的目的

2)引用只有一级,而指针可以有多级

3)指针传参的时候,还是值传递,指针本身的值不可以修改,需要通过解引用才能对指向的对象进行操作;引用传参的时候,传进来的就是变量本身,因此变量可以被修改。

结构体struct和共同体union(联合)的区别

结构体:将不同类型的数据组合成一个整体,是自定义类型
联合体:不同类型的几个变量共同占用一段内存
1)结构体中每个成员具有自己独立的地址,它们同时存在;联合体的成员共用一个地址,不能同时存在。
2)sizeof;sizeof(struct)是指内存对齐后,所有成员长度的总和;而sizeof(union)指的是内存对齐后,最长的数据成员长度。

内存对齐

宏定义

  • #define命令是一个宏定义命令,它用来将一个标识符定义为一个字符串,该标识符被称为宏名,被定义的字符串称为替换文本

  • 该命令有两种格式:一种是简单的宏定义,另一种是带参数的宏定义

  • 1)不带参数的宏:
           ~~~~~~       格式:#define 宏名 字符串 如:#define PI 3.1415926

           ~~~~~~       说明:
           ~~~~~~        1. 宏名一般都大写
           ~~~~~~        2. 宏的处理在预处理这一步,而语法检测在编译步骤,故而不进行语法检测
           ~~~~~~        3. 宏不会分配内存,变量会占用内存
           ~~~~~~        4. 字符串" "中永远不包含宏 —— 即 #define S “abc” “s” 不被替换
           ~~~~~~        5. 宏可以嵌套
           ~~~~~~        6. 可以用#undef命令终止宏定义的作用域

  • 2)带参数的宏:
    格式:#define 宏名(参数表) 字符串 如#define S(a,b) a*b
    area=S(3,2);第一步被换为area=ab; ,第二步被换为area=32; —— 类似于函数调用

     ~~~~     说明:

  1. 实参如果是表达式容易出问题: #define S(r) r*r
    area=S(a+b);第一步换为area=rr;,第二步被换为**area=a+ba+b**; —— 简单替换
    正确的宏定义是#define S(r) ((r)*(r))
  2. 宏替换只作替换,不做计算,不做表达式求解
  3. 宏名和参数的括号间不能有空格
  4. 函数调用在编译后程序运行时进行,并且分配内存。宏替换在编译前进行,不分配内存
  5. 宏不存在类型,也没有类型转换
  6. 宏展开不占运行时间只占编译时间,函数调用占运行时间(分配内存、保留现场、值传递、返回值

3)有参宏定义中#的用法:

如:#define STR(str) #str

#用于把宏定义中的参数两端加上字符串的""

比如,这里STR(my#name)会被替换成"my#name"

4)有参宏定义中##的用法

如:#define WIDE(str) L##str

则会将形参str的前面加上L

比如:WIDE(“abc”)就会被替换成L"abc"

如果有#define FUN(a,b) vo##a##b()

那么FUN(id ma,in)会被替换成void main()

#define 和 const 的区别

1)#define定义的常量没有类型,所给出的是一个立即数;const定义的常量有类型名字,存放在静态区域

2)处理阶段不同,#define定义的宏变量在预处理时进行替换,可能有多个拷贝,const所定义的变量在编译时确定其值,只有一个拷贝。

3)#define定义的常量是不可以用指针去指向,const定义的常量可以用指针去指向该常量的地址

4)#define可以定义简单的函数,const不可以定义函数

typdef 和 #define区别

#define是预处理命令,在预处理是执行简单的替换,不做正确性的检查

typedef是在编译时处理的,它是在自己的作用域内给已经存在的类型一个别名

typedef (int*) pINT;

#define pINT2 int*

效果相同?实则不同!实践中见差别:pINT a,b;的效果同int *a; int *b;表示定义了两个整型指针变量。而pINT2 a,b;的效果同int *a, b;表示定义了一个整型指针变量a和整型变量b

内联函数 和 普通函数

内联函数和普通函数最大的区别在于内部的实现方面,而不是表面形式,我们知道普通函数在被调用时,系统首先要 跳跃到该函数的入口地址,执行函数体,执行完成后,再返回到函数调用的地方,函数始终只有一个拷贝; 而内联函数则不需要进行一个寻址的过程,当执行到内联函数时,此函数展开(很类似宏的使用),如果在 N个地方调用了此内联函数,则此函数就会有N个代码段的拷贝

从内联函数的调用来看,它因为少了一个寻址过程而提高了代码的执行效率,但是这是以空间的代价来换取的;

其余等同于普通函数;

lambda 函数(匿名函数)

  • 格式:[capture](parameters)->return-type{body}
  • 如果没有参数,空的圆括号()可以省略。返回值也可以省略,如果函数体只由一条return语句组成或返回类型为void的话
[](int x, int y) { return x + y; } 	    // 隐式返回类型
[](int& x) { ++x; }                     // 没有return语句 -> lambda 函数的返回类型是'void'
[]() { ++global_x; }                   // 没有参数,仅访问某个全局变量
[]{ ++global_x; }                      // 与上一个相同,省略了()
[](int x, int y) -> int { int z = x + y; return z; }  //指定返回类型
  • Lambda函数可以引用在它之外声明的变量。这些变量的集合叫做一个闭包,闭包被定义在Lambda表达式声明中的方括号[]内
[]        //未定义变量.试图在Lambda内使用任何外部变量都是错误的.
[x, &y]   //x 按值捕获, y 按引用捕获.
[&]       //用到的任何外部变量都隐式按引用捕获
[=]       //用到的任何外部变量都隐式按值捕获
[&, x]    //x显式地按值捕获. 其它变量按引用捕获
[=, &z]   //z按引用捕获. 其它变量按值捕获

比如:

std::vector<int> some_list;
  int total = 0;
  int value = 5;
  std::for_each(begin(some_list), end(some_list), [&, value, this](int x) 
  {
    total += x * value * this->some_func();
  });

此例中total会存为引用,value则会存一份值拷贝。对this的捕获比较特殊,它只能按值捕获

this只有当包含它的最靠近它的函数不是静态成员函数时才能被捕获。对protect和priviate成员来说,这个lambda函数与创建它的成员函数有相同的访问控制

如果this被捕获了,不管是显式还隐式的,那么它的类的作用域对Lambda函数就是可见的。访问this的成员不必使用this->语法,可以直接访问

重载overload,覆盖override,重写overwrite,这三者之间的区别

1)overload,将语义相近的几个函数用同一个名字表示,但是参数和返回值不同,这就是函数重载

特征:相同范围(同一个类中)、函数名字相同、参数不同、virtual关键字可有可无

2)override,派生类覆盖基类的虚函数,实现接口的重用 ——实现动态的多态性

特征:不同范围(基类和派生类)、函数名字相同、参数相同、基类中必须有virtual关键字(必须是虚函数)

3)overwrite,派生类屏蔽了其同名的基类函数

特征:不同范围(基类和派生类)、函数名字相同、参数不同或者参数相同且无virtual关键字

delete和delete[ ] 的区别:

delete只调用一次析构函数,而delete[]调用每个成员的析构函数。用new分配的内存使用delete来释放,new[]使用delete[]。

若不delete会发生内存泄漏,什么叫内存泄漏?

动态分配内存所开辟的空间,在使用完毕后未手动释放,导致一直占据该内存,即为内存泄漏。

方法:malloc/free要配套,对指针赋值的时候应该注意被赋值的指针是否需要释放;使用的时候记得指针的长度,防止越界

STL库

1)容器:用于存储数据;
顺序容器(list,vector);关联容器(map,set);
2)迭代器:泛化的指针,它能够按照某种顺序依次访问容器中的各个数据,而不暴露容器的内部结构;使算法和容器相互独立,同时也是算法和容器的桥梁;
迭代器的使用: 容器类型<数据类型>::iterator 迭代器名
3)算法:各个容器具有其特有的算法;

map和set(红黑树):

map和set的底层实现主要通过红黑树来实现

红黑树是一种特殊的二叉查找树

1)每个节点或者是黑色,或者是红色

2)根节点是黑色

3) 每个叶子节点(NIL)是黑色。 [注意:这里叶子节点,是指为空(NIL或NULL)的叶子节点!]

4)如果一个节点是红色的,则它的子节点必须是黑色的

5)从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。

特性4)5)决定了没有一条路径会比其他路径长出2倍,因此红黑树是接近平衡的二叉树

关于const:

1.指针常量:
1)指向常量的指针:
const int *p; //指针指向常量,指针指向可变但其指向的常量不可变;
如:

const int a=3;
int b=4;
const int *p=&a;
p=&b;   // 正确,可改变指向的变量;
*p=b;  //错误

2)常量指针: 指针指向不能改变,但该地址中的值可以改变;

int a=3;
int const *p=&a;
int b=5;
*p=b;  //正确
p=&a; //错误

2.const修饰类的成员变量,表示常量不可能被修改
3.const修饰类的成员函数,表示该函数不会修改类中的数据成员,不会调用其他非const的成员函数

static的作用:

1)函数体内: static 修饰的局部变量作用范围为该函数体,不同于auto变量,其内存只被分配一次,因此其值在下次调用的时候维持了上次的值

2)模块内:static修饰全局变量或全局函数,可以被模块内的所有函数访问,但是不能被模块外的其他函数访问,使用范围限制在声明它的模块内

3)类中:修饰成员变量,表示该变量属于整个类所有,对类的所有对象只有一份拷贝;(注意其初始化在类外,格式为<数据类型><类名>::<静态数据成员名>=<值>)

4)类中:修饰成员函数,表示该函数属于整个类所有,不接受this指针,只能访问类中的static成员变量

注意和const的区别!!!const强调值不能被修改,而static强调唯一的拷贝,对所有类的!

#include< file.h> 和 #include file.h的区别?

#include<file.h>:在标准路径中寻找
#include file.h:在当前工作路径下寻找

STL库中vector 是如何扩容的?

vector就是一个动态增长的数组,里面有一个指针指向一片连续的空间,当空间装不下的时候,会申请一片更大的空间,将原来的数据拷贝过去,并释放原来的旧空间。当删除的时候空间并不会被释放,只是清空了里面的数据。对比array是静态空间一旦配置了就不能改变大小。

在原来空间不够存储新值时,每次调用push_back方法都会重新分配新的空间以满足新数据的添加操作。如果在程序中频繁进行这种操作,还是比较消耗性能的。

构造函数为什么一般不定义为虚函数?而析构函数一般写成虚函数的原因 ?

1.构造函数不定义为虚函数:

  • 构造函数在创建对象的时候必须确定对象的类型,若用虚函数其是在运行时确定对象类型的,在创建的时候无法确定对象是属于基类还是派生类。
  • 虚函数的调用需要一个虚函数表指针,而该指针存放在对象的内存中。若声明为虚函数,未创建对象,无该虚函数指针。

2.析构函数最好是虚函数

  • 析构函数可以是虚函数,且当析构指向派生类的基类指针时,最好对基类的析构函数使用虚函数,防止出现内存泄漏。
  • 将基类的析构函数声明为虚函数,则会调用基类和派生类的析构函数。
  • 若不将其声明为虚函数,静态编译,则只会调用基类的析构函数而不调用派生类的析构函数,造成内存泄漏。

静态绑定和动态绑定的介绍

  • 静态绑定:在编译时确定,如重载
  • 动态绑定:在运行时确定,如虚函数
    (用引用或指针结合虚函数可以实现动态绑定)

引用作为函数参数以及返回值的好处

1.可以在函数内部对参数进行修改;
2.提高函数调用和运行的效率;

在函数调用的时候,将实参传递给形参,实际上相当于生成了一个实参副本。在函数内部进行操作时,是对这个副本进行操作和修改,不影响原本的实参。若采用引用则可以实时对原本实参进行修改,而不需要返回实参副本。

引用使用的限制:
1.不能返回局部变量的引用,因为局部变量在返回后就消亡了;
2.不能返回使用new的变量,可能造成内存泄漏;
3.可以返回类的对象的引用,但最好是const形式,防止其值被修改。

深拷贝和浅拷贝:

对一个类进行复制,若类的资源进行了重新分配则其实深拷贝,否则其为浅拷贝。

什么情况下调用拷贝构造函数?

  • 用类的对象去初始化另一个对象;
  • 函数的形参是类的对象,且是值传递;(引用传递不会调用拷贝构造函数)
  • 函数的返回值是类的对象;
#include <iostream>
#include <string>
 
using namespace std;
 
class A{
	private:
		int data;
	public:
		A(int i){ data = i;} 	//自定义的构造函数
		A(A & a);  			//拷贝构造函数 
		int getdata(){return data;} 
};
//拷贝构造函数 
A::A(A && a){
	data = a.data;
	cout <<"拷贝构造函数执行完毕"<<endl;
}
//参数是对象,值传递,调用拷贝构造函数
int getdata1(A a){
	return a.getdata();
}
//参数是引用,引用传递,不调用拷贝构造函数
int getdata2(A &a){
	return a.getdata();
} 
//返回值是对象类型,会调用拷贝构造函数
 A getA1(){
 	A a(0);
 	return a;
 } 
 //返回值是引用类型,会调用拷贝构造函数,因为函数体内生成的对象是临时的,离开函数就消失
 A& getA2(){
 	A a(0);
 	return a;
 } 
 
 int main(){
    A a1(1);  
    A b1(a1);           		//用a1初始化b1,调用拷贝构造函数  
    A c1=a1;            		//用a1初始化c1,调用拷贝构造函数  
  
    int i=getdata1(a1);        	//函数形参是类的对象,调用拷贝构造函数  
    int j=getdata2(a1);      	//函数形参类型是引用,不调用拷贝构造函数  
  
    A d1=getA1();       		//调用拷贝构造函数  
    A e1=getA2();     			//调用拷贝构造函数  
  
    return 0;  
}  


纯虚函数

纯虚函数是只有声明的虚函数,是一个接口;只有子类实现了这个纯虚函数才能生成对象。

野指针

定义:野指针不是NULL指针,它是未初始化或者未清零的指针,其指向的内存不是程序员所期待的,有可能指向了受限的内存。
成因:

  • 未初始化指针
  • 指针指向的内存释放了,但指正未置成NULL(未释放)
  • 指向超出了作用范围,如p[10], 指针p+11

线程安全和线程不安全

线程安全就是多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问直到该线程读取完,其他线程才可以使用,不会出现数据不一致或者数据污染。

线程不安全就是不提供数据访问保护,有可能多个线程先后更改数据所得到的数据就是脏数据。

vector中resize() 和 reserve() 函数的区别

  • reserve 容器预留空间 ,但并不真正创建对象,在创建对象前,不能引用容器内元素, 因此加入元素后,需要push_baxck() 和insert()函数
  • resize() 改变容器大小,并且创建对象。调用函数后,可以直接引用容器内对象。
  • reserve()只有一个参数, 即需要预留容器的空间
  • resize() 两个参数,第一个容器新的大小,第二个是加入容器中的新元素
  • 共同点是:都保证了vector中空间(capacity)的大小,但是resize() 会调整 size() 的大小

原文:https://blog.csdn.net/ljh0302/article/details/81098764
C++面经 TCP/iP ,进程线程、堆栈

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值