【C++】面试经历小点总结

文章目录

C++基础

1、 理解常用内存类型

  • 程序员分配释放,如果一味分配而不释放就会造成内存泄漏。容量取决于虚拟内存,较大。

  • 编译器自动分配释放,其中存放在主调函数中被调函数的下一句代码,函数参数和局部变量,容量有限,较小。
  • 局部变量默认类型是auto,从栈中分配(auto通常是指变量进入其作用域就会被分配,离开作用域就会被释放

静态存储区

  • 编译时编译器分配,系统回收。其上存放全局变量,static变量和常量。
  • static变量都存放在静态存储区
  • 变量在程序初始化时被分配,知道程序退出才被释放

区别

  • 地址扩展:堆是低地址向高地址扩展,栈是高地址向低地址扩展
  • 空间连续:堆是不连续的空间,栈是连续的空间
  • 申请空间快慢:在申请空间后,栈的分配要比堆的快。对于堆,先遍历存放空闲存储地址的链表,修改链表,在进行分配。对于栈,只要剩下的空间足够用,就可分配到,否则就报栈溢出。
  • 生命周期:栈的生命周期最短,到函数调用结束时。静态存储声明周期最长,到程序结束时。堆声明周期被我们手动释放(如果整个过程不释放,就到程序结束时)

2、static

1) 扩展生命周期
局部变量被static修饰后,生存期不再是当前作用域,而是整个程序的生存期。

2) 限制作用域
① 普通的全局变量和函数,其作用域为整个程序或项目,外部文件可以通过extern关键字访问到该变量和函数。

在头文件里声明为externextern int g_value; // 不要初始化

int g_value = 0; // 初始化

包含该头文件的cpp文件都可以用g_value这个名字访问相同的一个变量

static全部变量和函数,作用域为当前文件,其他文件不能访问

3)数据唯一性
静态成员与普通成员的区别:属于一个类而不属于此类的任何特定对象的变量和函数

static成员在类外初始化。初始化时要标明所属类。

static成员函数:静态成员函数只能访问静态数据成员,不能访问非静态数据成员,也不能访问非静态的成员函数,因为没有this指针。
 

3、const

int * const p = &a;
// p 其指向的内存地址不能够被改变,但a内容可以改变
int const* p = &a;
// 指针指向的内容 a 不可改变

const关键字的作用

  • 定义const变量时,需要初始化
  • const修饰形参,在函数内部不能改变
  • const修饰类的成员函数,标明其是一个常函数,不能修改类的成员变量
  • 指定的返回值为const类型,标明返回值不为“左值”

const 函数中的使用

void func(const int a);
// 传递的参数在函数中不可改变

void func(const char* a);
// 参数所指向的内容为常量不可变

void func(const T& a);
// 提高效率且不能修改

const 修饰返回值

const char* func(void);
// 调用
const char* str = func();
// 返回值只能被赋给加const 修饰的同类型指针。

const 类相关
成员变量

class A
{ 
    const int nValue;         //成员常量不能被修改
    A(int x): nValue(x) { } ; //只能在初始化列表中赋值
} 

成员函数

class A
{ 
    void function()const; //常成员函数, 它不改变对象的成员变量.                        
    //也不能调用类中任何非const成员函数。
}
  • const对象只能访问const成员函数,非const对象可以访问任意成员函数
  • const对象的成员是不可以修改的,但const对象通过指针维护的对象是可以修改的
  • const成员函数不能修改对象的数据
  • mutable修饰符的数据成员,对于任何情况下通过任何手段都可修改

const修饰类对象/对象指针/对象引用
const修饰类对象表示该对象为常量对象,其中的任何成员都不能被修改。对于对象指针和对象引用也是一样。
const修饰的对象,该对象的任何非const成员函数都不能被调用。

将const转换成非const
采用const_cast 进行类型转换。

 

4、volatile

易变的
在 C/C++ 语言中,volatile 的易变性体现在:假设有读、写两条语句,依次对同一个 volatile 变量进行操作,那么后一条的读操作不会直接使用前一条的写操作对应的 volatile 变量的寄存器内容,而是重新从内存中读取该 volatile 变量的值。

不可优化的
volatile 会告诉编译器,不要对 volatile 声明的变量进行各种激进的优化,从而保证程序员写的代码中的指令一定会被执行。

顺序执行的
能够保证 volatile 变量间的顺序性,不会被编译器进行乱序优化。

 

5、extern

extern "C"

extern "C" void fun();
// 标明这个函数按照C的规则去翻译函数

头文件中使用

extern int g_val;
// 声明变量或函数,表明该变量或函数可以在本文件或者其他文件中使用

 

6、运算符重载

关系运算符

bool operator == (const A&a1, const A&a2); 
bool operator != (const A&a1, const A&a2);
bool operator < (const A&a1, const A&a2);
bool operator <= (const A&a1, const A&a2 );
bool operator > (const A&a1, const A&a2 );
bool operator >= (const A&a1, const A&a2);

单目运算符

A& operator + (const A&a1, const A&a2);
A& operator - (const A&a1, const A&a2);
A* operator & (const A&a1, const A&a2);
A& operator * (const A&a1, const A&a2);

自增减运算符

A& operator ++ ();//前置++
A operator ++ (int);//后置++
A& operator --();//前置--
A operator -- (int);//后置--

6.4 位运算符重载

A operator | (const A&a1, const A&a2);
A operator & (const A&a1, const A&a2);
A operator ^ (const A&a1, const A&a2 );
A operator << (int i);
A operator >> (int i);
A operator ~ ();

赋值运算符重载

A& operator += (const A& );
A& operator -= (const A& ); 
A& operator *= (const A& );
A& operator /= (const A& );
A& operator %= (const A& );
A& operator &= (const A& );
A& operator |= (const A& );
A& operator ^= (const A& );
A& operator <<= (int i);
A& operator >>= (int i);

内存运算符

void *operator new(size_t size);
void *operator new(size_t size, int i);
void *operator new[](size_t size);
void operator delete(void*p);
void operator delete(void*p, int i, int j);
void operator delete [](void* p);

 

7、malloc和new

malloc和new的区别
1)所属语言
new是C++关键字,malloc是C的。

2)申请释放方式
new与delete,malloc与free配对使用。

3)malloc标准库函数,new是C++的运算符
new可以被重载,malloc不可以。

4)析构与构造
new申请自动调用构造函数,delete自动调用析构函数,malloc不行。

5)申请空间失败
new失败抛异常,malloc返回NULL

6)重新分配内存
malloc可利用realloc重新分配,new不行。

7)类型安全性
new会检查类型是否对应,malloc只会申请内存的多少,不会检查。

8)类型转换
malloc返回值为void,malloc需要进行类型转换,new不需要。

9)数组分配
new有明确的数组处理方式,即new[],释放时delete[],malloc没有。

10)设置内存分配器
new可以设置自己的内存分配器,malloc不行。

 

8、宏定义和内联函数

  1. 安全性能:宏定义不检查函数参数,返回值等,内联函数会检查参数类型,所以更安全。

  2. 处理阶段:宏是由预处理器对宏进行替代,而内联函数是通过编译器控制来实现的。

  3. 内联函数是真正的函数,只是在需要用到的时候,内联函数像宏一样的展开,所以取消了函数的参数压栈,减少了调用的开销。

9、逻辑地址和物理地址

  • 物理地址:加载到内存地址寄存器中的地址,内存单元的真正地址。在前端总线上传输的内存地址都是物理内存地址,编号从0开始一直到可用物理内存的最高端。这些数字被北桥(Nortbridge chip)映射到实际的内存条上。
  • 逻辑地址CPU所生成的地址。逻辑地址是内部和编程使用的、并不唯一。例如,你在进行C语言指针编程中,可以读取指针变量本身值(&操作),实际上这个值就是逻辑地址,它是相对于你当前进程数据段的地址(偏移地址),不和绝对物理地址相干。

10、进程和线程区别

  1. 调度:进程是对运行程序的封装,是系统进行资源分配和调度的基本单元,而线程是进程的子任务,是CPU分配和调度的基本单元
  2. 一个进程可以有多个线程,但一个线程只能属于一个进程
  3. 系统开销:进程创建需要系统分配内存和CPU,文件句柄等资源,销毁时也要进行相应的回收,所以进程管理开销很大;但是线程的管理开销很小。
  4. 进程之间不会相互影响;而一个线程的崩溃会导致进程崩溃,从而影响同个进程的其他线程。
  5. 并发性:不仅进程之间可以并发执行,同一个进程的多个线程之间也可并发执行。
  6. 拥有资源:进程是拥有资源的一个独立单位,线程不拥有系统资源,但可以访问隶属于进程的资源。

多线程之间的独有与共享
1)独有:线程ID(都有一个在本进程中唯一的ID),堆栈(保证线程独立运行) , 寄存器(上下文信息),调度优先级,信号屏蔽字,错误码

2)共享:虚拟地址空间(代码段、数据段),文件描述符,信号处理方式,工作路径,用户组ID/组ID

11、返回值,全局变量和异常的错误处理方式比较

优点缺点
返回值和系统API一样不能计算出返回值
全局变量能计算出返回值用户可能会忘记检查
异常可以为不同的错误定义不同的类型有些语言不支持异常

12、引用和指针的区别

1)引用必须初始化,指针没有要求

2)没有空引用,有空指针

3)引用只能初始化一个实体,但是指针任何时候指向同一个类型的实体

4)引用大小是引用实体的大小,指针是四个字节(32位)

5)引用自加即实体加一,指针自加即指向向后偏移一位

6)有多级指针,没有多级引用

7)访问实体不同:指针显式解引用,引用编译器自己处理

8)引用比指针更安全,简洁

 

13、常见的内存错误

1)内存分配未成功
使用内存之前进行 if(p == NULL)防错处理

2)内存分配成功,未初始化
无论何种方式创建的数组,都需要进行赋初值

3)内存分配成功且初始化,但操作越界
for循环注意越界

4)忘记释放内存,造成内存泄漏
动态内存申请和释放必须配对,程序中malloc / freenew / delete配对使用

5)释放了内存却继续使用它
free或delete释放了内存后,没有将指针设置为NULL,就会造成野指针。

 

14、野指针的成因

1)指针变量没有被初始化
2)指针p被释放后,没将p设置为NULL
3)指针操作超越了变量的作用域范围
 

15、两种指针参数传递内存

void GetSpace(char** p, int size)
{
	*p = (char*)malloc(sizeof(char)*size);
}

如果传递的是一级指针,函数每个参数都会有一个临时拷贝,_p = p,_p 申请了新的内存,但只把 _p 所指的内存地址改变了,p的未改变。

char* GetSpace(int size)
{
	char* p = (char*)malloc(sizeof(char)*size);
	return p;
}

注意:return返回的指向不能是栈内存的指针,因为栈内存在函数结束时会自动消亡。
 

16、常见的ASCII

十进制字符 / 缩写解释
0NUL空字符
13CR回车键
42*
43+
45-
47/
480
579
60<
61=
62>
65A
90Z
97a
122z
126~
127DEL删除

17、数组指针和指针数组

数组指针(行指针)
int (*p)[n];首先说明,p是一个指针,指向一个整型的一维数组。一维数组的长度是n,也就是p的步长。

// 二维数组赋值给一指针
int a[3][4];
int (*p)[4];
p=a;   // 将二维数组的首地址赋值给p
p++;   // a[0][] --->  a[1][]

数组指针:首先它是一个指针,它指向一个数组。在32位操作系统下,任何类型的指针占4个字节。

指针数组
int *p[n];[]的优先级较高,所以先与p结合成一个数组,再由 int* 说明这是一个整型指针数组。

// 将二维数组赋值给一指针数组
int*p[3];
int a[3][4];
for(int i = 0; i < 3; ++i)
{
	p[i] = a[i];
}

指针数组:首先它是一个数组,数组中的元素都是指针,数组占多少个字节由数组本身决定。

在这里插入图片描述

18、面向对象三大特性

面向对象的三个基本特征是:封装、继承、多态。其中,封装可以隐藏实现细节,使得代码模块化;继承可以扩展已存在的代码模块(类);它们的目的都是为了——代码重用。而多态则是为了实现另一个目的——接口重用

封装
封装可以隐藏实现细节,使得代码模块化;封装是把过程和数据包围起来,对数据的访问只能通过已定义的界面。把客观的事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的类进行信息的隐藏。

简单的说就是:封装使对象的设计者与对象的使用者分开,使用者只要知道对象可以做什么就可以了,不需要知道具体是怎么实现的。封装可以有助于提高类和系统的安全性。

继承
它可以使用现有类的所有功能,并在无需重新编写原来的类的情况下,对这些功能进行扩展。其继承的过程,就是从一般到特殊的过程,能够很好的实现代码的重用性。

面向对象三大特性----继承

多态
子类对象可以赋值给父类变量,但运行时仍表现出子类的行为特征,这意味着同一个类型的对象在执行同一个方法时,可以表现出多种行为特征。基类中必须要有虚函数,而且子类必须要对基类中的虚函数进行重写

虚函数调用:必须(基类)指针或者引用调用虚函数

面向对象的三大特性—多态

 

19、C++各种继承规则

1)public(公有继承):继承时保持基类中各成员属性不变,并且基类中private成员被隐藏。派生类的成员只能访问基类中的public/protected成员,而不能访问private成员;派生类的对象只能访问基类中的public成员。

2)private(私有继承):继承时基类中各成员属性均变为private,并且基类中private成员被隐藏。派生类的成员也只能访问基类中的public/protected成员,而不能访问private成员;派生类的对象不能访问基类中的任何的成员。

3)protected(保护性继承):继承时基类中各成员属性均变为protected,并且基类中private成员被隐藏。派生类的成员只能访问基类中的public/protected成员,而不能访问private成员;派生类的对象不能访问基类中的任何的成员。
 

20、生产者消费者模型

123规则
1个线程安全的队列

  • 先进先出(所有满足先进先出的特性都可以称之为队列)
  • 线程安全:保证同一时刻,队列中的元素只有一个执行流去访问(互斥锁+条件变量)

2种角色的线程

  • 生产线程
  • 消费线程

3种关系

  • 生产者与生产者互斥
  • 消费者与消费者互斥
  • 生产者与消费者同步+互斥

优点:
1)耦开:降低生产者和消费者之间的依赖关系。

2)支持高并发:生产者和消费者可以是两个独立的并发主体,互不干扰的运行。

3)支持忙闲不均:如果制造数据的速度时快时慢,缓冲区可以对其进行适当缓冲。当数据制造快的时候,消费者来不及处理,未处理的数据可以暂时存在缓冲区中。等生产者的制造速度慢下来,消费者再慢慢处理掉。
 

21、LRU算法原理

在计算中,所有的文件操作都要放在内存中进行,然而计算机内存大小是固定的,所以我们不可能把所有的文件都加载到内存,因此我们需要制定一种策略对加入到内存中的文件进项选择。

LRU的设计原理:对于经常访问的数据,我们需要能够快速命中,而不常访问的数据,当超出容量限制,要将其淘汰。

常见的页面置换算法有如下几种:

LRU 最近最久未使用
FIFO 先进先出置换算法 类似队列
OPT 最佳置换算法 (理想中存在的)
NRU Clock置换算法
LFU 最少使用置换算法
PBA 页面缓冲算法

链接:
https://leetcode-cn.com/problems/lru-cache/

 

22、HTTP和HTTPS的区别

HTTPS和HTTP的区别:

  1. https协议需要到ca申请证书,一般免费证书较少,因而需要一定费用。
  2. http是超文本传输协议,信息是明文传输,https则是具有安全性的ssl加密传输协议。
  3. http和https使用的是完全不同的连接方式,http端口是80,https端口是443。
  4. http的连接很简单,是无状态的;HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,比http协议安全。

 

23、交换a和b三种方法比较

1)临时变量交换a和b
这种方法的优点是极其稳定,几乎不用担心越界等一系列错误

2)先相加后相减的方法
优点:没有临时变量且较为稳定
缺点:有可能涉及到溢出,但是可能性很小。

3)抑或
优点:比较高端
缺点:出错的概率相较于第二种方法更大

 

24、C++为什么引入new

对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数。由于malloc/free是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于malloc/free。因此C++语言需要一个能完成动态内存分配和初始化工作的运算符new,以一个能完成清理与释放内存工作的运算符delete。

 

25、动态库和静态库

静态库:在链接阶段,会将汇编生成的目标文件.o与引用到的库一起链接打包到可执行文件中。因此对应的链接方式称为静态链接。

静态库优点

  • 静态库对函数库的链接是放在编译时期完成的。
  • 程序在运行时与函数库再无瓜葛,移植方便。

静态库缺点

  • 空间浪费是静态库的一个问题。
  • 静态库对程序的更新、部署和发布页会带来麻烦。如果静态库libxx.lib更新了,所有使用它的应用程序都需要重新编译、发布给用户(对于玩家来说,只是一个很小的改动,却导致整个程序重新下载,全量更新)。

动态库:动态库在程序编译时并不会被连接到目标代码中,而是在程序运行是才被载入。不同的应用程序如果调用相同的库,那么在内存里只需要有一份该共享库的实例,规避了空间浪费问题。动态库在程序运行时才被载入,也解决了静态库对程序的更新、部署和发布页会带来麻烦。用户只需要更新动态库即可,增量更新。

动态库特点

  • 动态库把对一些库函数的链接载入推迟到程序运行的时期。
  • 可以实现进程之间的资源共享。(因此动态库也称为共享库)
  • 将一些程序升级变得简单。
  • 甚至可以真正做到链接载入完全由程序员在程序代码中控制(显示调用)。

 

26、移动构造函数与拷贝构造函数区别

http://c.biancheng.net/view/7847.html

 

27、空类的大小为一个字节

C++标准指出,不允许一个对象的大小为0.不同的对象不能具有相同的地址,① 由于new需要分配不同的内存地址,不能分配大小为0的空间 ② 避免sizeof(T)时除以0的错误。

继承体系中,子类如果有数据成员,而空父类的一个字节不会加到子类中。

空类中含有this指针,编译器会传入一个this指针,该指针为null,没有用到程序没问题,用到就会程序崩溃。

 

28、程序编译的四个阶段

预处理阶段:头文件展开(<>文件在INCLUDE目录下寻找,"" 文件当前目录下寻找),宏替换,注释消除,将头文件中的内容直接插入到程序文本中,得到一个C语言程序,通常以.i 为文件扩展名。 gcc -E hello.c -o hello.i

编译阶段:生成汇编文件,主要进行词法分析、语法分析、语义分析 gcc -S hello.i -o hello.s

汇编阶段:汇编变成目标代码,生成.o文件(将对应的汇编指令翻译成机器指令,生成可重定位的二进制文件) gcc -c hello.s -o hello.o

链接阶段:链接器(ld)负责处理合并目标代码,生成一个可执行目标文件,可以被加载到内存中,由系统执行。 gcc hello.o -o hello
 

29、C++与C的区别

面试题——C/C++的区别与联系
C++经典面试题 | C和C++的区别是什么?

30、struct和class的区别

C和C++中struct的区别是什么?
(1) C语言的struct不能有函数成员,而C++的struct可以有。

(2) C语言的struct中数据成员没有private、public和protected访问权限的设定,而C++ 的struct的成员有访问权限设定

(3) C语言的struct是没有继承关系的,而C++的struct却有丰富的继承关系

C++中的 struct与class的区别是什么?
(1) 默认继承权限不同。class继承默认是private继承,而struct继承默认是public继承;

(2) class还用于定义模板参数,就像typename,但关键字struct不用于定义模板参数
 

31、C++四种类型转换

关键字说明
static_cast原有的自动类型转换、向上转型
dynamic_castdynamic_cast 用于在类的继承层次之间进行类型转换,它既允许向上转型,也允许向下转型。向上转型是无条件的,不会进行任何检测,所以都能成功;向下转型的前提必须是安全的,要借助 RTTI 进行检测,所有只有一部分能成功。
const_cast用于 const 与非 const、volatile 与非 volatile 之间的转换
reinterpret_castreinterpret_cast 可认为是 static_cast 的一种补充,例如两个具体类型指针之间的转换、int 和指针之间的转换

dynamic_cast 与 static_cast 是相对的,dynamic_cast 是“动态转换”的意思,static_cast 是“静态转换”的意思。dynamic_cast 会在程序运行期间借助 RTTI 进行类型转换,这就要求基类必须包含虚函数;static_cast 在编译期间完成类型转换,能够更加及时地发现错误。
 

32、struct内存对齐原则

1)存储单元是否为所有元素中最宽的元素(最长的基本类型)的长度的整数倍
2)元素放置的起始位置一定会在自己宽度的整数倍上开始
3)结构体总体大小能够被最宽的成员的大小整除,如不能则在后面补充字节

 

33、静态多态和动态多态

静态多态:也称为编译期间的多态,编译器在编译期间完成的,编译器根据函数实参的类型(可能会进行隐式类型转换),可推断出要调用那个函数,如果有对应的函数就调用该函数,否则出现编译错误。静态多态有两种实现方式:

  • 函数重载:包括普通函数的重载和成员函数的重载
  • 函数模板

动态多态:即运行时的多态,在程序执行期间(非编译期)判断所引用对象的实际类型,根据其实际类型调用相应的方法。

1)通过基类类型的引用或者指针调用虚函数

  • 静态类型:对象声明时的类型,编译时确定
  • 动态类型:目前所指对象的类型,运行时确定
    在这里插入图片描述

2)派生类一定要重写基类中的虚函数
在这里插入图片描述

34、为什么C语言中没有重载

1)C编译器的函数名修饰规则

__stdcall调用约定:编译器和链接器会在输出函数名加上一个下划线前缀,函数名后面加上一个“@”符号和其參数的字节数。例如:_functionname@number。

__cdecl调用约定:在输出函数名前加上一个下划线前缀。例如:_functionname。

__fastcall调用约定:在输出函数名加上一个“@”符号和其參数的字节数,比如 @functionname@number

2)C++编译器的函数名修饰规则

C++的函数名修饰规则有些复杂。可是信息更充分,通过分析修饰名不仅可以知道函数的调用方式。返回值类型,參数个数甚至參数类型。

无论 __cdecl,__fastcall还是__stdcall调用方式,函数修饰都是以一个**“?” 开始,后面紧跟函数的名字,再后面是參数列表的开始标识和依照参数类型代号拼出的参数表。**

参数表开始符号:

  • __stdcall:@@YG
  • __cdecl:@@YA
  • __fastcall:@@YI

参数表拼写代号:
X–void、D–char、E–unsigned char、F–short、H–int、I–unsigned int、J–long 、K–unsigned long(DWOR)、M–float、N–double、_N–bool、U–struct

指针:用PA表示指针,用PB表示const类型的指针。

样例1:
int Function1 (char *var1,unsigned long);
其函数修饰名为“?Function1@@YGHPADK@Z”。

样例2:
void Function2();
其函数修饰名则为“?Function2@@YGXXZ” 。

 

35、野指针的产生与避免

野指针的产生原因

1)指针定义时未被初始化:指针在被定义的时候,如果程序不对其进行初始化的话,它会随机指向一个区域

2)指针被释放时没有置空:指针指向的内存空间在释放后,如果程序员没有对其进行置空或者其他赋值操作的话,就会成为一个野指针

3)指针操作超越变量作用域:不要返回指向栈内存的指针或者引用,因为栈内存在函数结束的时候会被释放

野指针的危害
指针指向的内存已经无效了,而指针没有被置空,解引用一个非空的无效指针是一个未被定义的行为。也就是说不一定导致段错误,野指针很难定位到是哪里出现的问题。

野指针的避免
1)初始化指针的时候将其置为nullptr。

2)释放指针的时候将其置为nullptr。

 

36、负载均衡算法

1)轮询(Round Robin)法

2)随机(Random)法
通过系统随机函数,根据后端服务器列表的大小值来随机选择其中一台进行访问。由概率统计理论可以得知,随着调用量的增大,其实际效果越来越接近于平均分配流量到每一台后端服务器,也就是轮询的效果。

3)源地址哈希(Hash)法
源地址哈希的思想是获取客户端访问的IP地址值,通过哈希函数计算得到一个数值,用该数值对服务器列表的大小进行取模运算,得到的结果便是要访问的服务器的序号。

4)加权轮询(Weight Round Robin)法
不同的服务器可能机器配置和当前系统的负载并不相同,因此它们的抗压能力也不尽相同,给配置高、负载低的机器配置更高的权重,让其处理更多的请求,而低配置、高负载的机器,则给其分配较低的权重,降低其系统负载。

5)加权随机(Weight Random)法
与加权轮询法类似,加权随机法也是根据后端服务器不同的配置和负载情况来配置不同的权重。不同的是,它是按照权重来随机选择服务器的,而不是顺序。

6)最小连接数(Least Connections)法
最小连接数算法比较灵活和智能,由于后端服务器的配置不尽相同,对于请求的处理有快有慢,它正是根据后端服务器当前的连接情况,动态地选取其中当前积压连接数最少的一台服务器来处理当前请求,尽可能地提高后端服务器的利用效率,将负载合理地分流到每一台机器。

 

37、fsync函数

延迟写(delayed write): 传统的UNIX实现在内核中设有缓冲区高速缓存或页面高速缓存,大多数磁盘I/O都通过缓冲进行。 当将数据写入文件时,内核通常先将该数据复制到其中一个缓冲区中,如果该缓冲区尚未写满,则并不将其排入输出队列,而是等待其写满或者当内核需要重用该缓冲区以便存放其他磁盘块数据时, 再将该缓冲排入到输出队列,然后待其到达队首时,才进行实际的I/O操作。这种输出方式就被称为延迟写。

延迟写减少了磁盘读写次数,但是却降低了文件内容的更新速度,使得欲写到文件中的数据在一段时间内并没有写到磁盘上。当系统发生故障时,这种延迟可能造成文件更新内容的丢失。为了保证磁盘上实际文件系统与缓冲区高速缓存中内容的一致性,UNIX系统提供了sync、fsync和fdatasync三个函数。

1、sync函数
sync函数只是将所有修改过的块缓冲区排入写队列,然后就返回,它并不等待实际写磁盘操作结束。

通常称为update的系统守护进程会周期性地(一般每隔30秒)调用sync函数。这就保证了定期冲洗内核的块缓冲区。命令sync(1)也调用sync函数。

2、fsync函数
fsync函数只对由文件描述符filedes指定的单一文件起作用,并且等待写磁盘操作结束,然后返回。

fsync可用于数据库这样的应用程序,这种应用程序需要确保将修改过的块立即写到磁盘上。

// 系统调用fsync将所有已写入文件描述符fd的数据真正的写道磁盘或者其他下层设备上。
#include <unistd.h>

int fsync(int fd); // fd:文件描述符。

3、fdatasync函数
fdatasync函数类似于fsync,但它只影响文件的数据部分。而除数据外,fsync还会同步更新文件的属性。

对于提供事务支持的数据库,在事务提交时,都要确保事务日志(包含该事务所有的修改操作以及一个提交记录)完全写到硬盘上,才认定事务提交成功并返回给应用层。

4、fflush:标准IO函数(如fread,fwrite等)会在内存中建立缓冲,该函数刷新内存缓冲,将内容写入内核缓冲,要想将其真正写入磁盘,还需要调用fsync。fflush以指定的文件流描述符为参数,仅仅是把上层缓冲区中的数据刷新到内核缓冲区就返回。

 

38、物理内存和虚拟内存的联系

在很久以前,还没有虚拟内存概念的时候,程序寻址用的都是物理地址。程序能寻址的范围是有限的,这取决于CPU的地址线条数。比如在32位平台下,寻址的范围是2^32也就是4G。

每次开启一个进程都给4G的物理内存,就可能会出现很多问题:
因为我的物理内存时有限的,当有多个进程要执行的时候,都要给4G内存,很显然你内存小一点,这很快就分配完了,于是没有得到分配资源的进程就只能等待。当一个进程执行完了以后,再将等待的进程装入内存。这种频繁的装入内存的操作是很没效率的。由于指令都是直接访问物理内存的,那么我这个进程就可以修改其他进程的数据,甚至会修改内核地址空间的数据,这是我们不想看到的。因为内存时随机分配的,所以程序运行的地址也是不正确的。

于是针对上面会出现的各种问题,虚拟内存就出来了。

进程得到的这4G虚拟内存是一个连续的地址空间(这也只是进程认为),而实际上,它通常是被分隔成多个物理内存碎片,还有一部分存储在外部磁盘存储器上,在需要时进行数据交换。

进程开始要访问一个地址,它可能会经历下面的过程

  1. 每次访问地址空间上的某一个地址,都需要把地址翻译为实际物理内存地址;
  2. 所有进程共享这整一块物理内存,每个进程只把自己目前需要的虚拟地址空间映射到物理内存上;
  3. 进程通过页表来记录哪些地址空间上的数据在物理内存上,哪些不在(可能这部分存储在磁盘上)
  4. 页表分两部分:第一部分记录此页是否在物理内存上;第二部分记录物理内存页的地址
  5. 当进程访问某个虚拟地址时,就会先查看页表,如果发现对应的数据不在物理内存上,就会发生缺页异常;
  6. 缺页异常的处理过程:操作系统立即阻塞该进程,并将硬盘里对应的页换入内存,然后使该进程就绪;如果内存已满,那就通过页面置换算法找一个页覆盖。

 

39、重载和重写

基本概念:

重载:是指同一作用域内被声明的几个具有不同参数列(参数的类型,个数,顺序不同)的同名函数,重载不关心函数返回类型。

隐藏:是指派生类的函数屏蔽了与其同名的基类函数,注意只要同名函数,不管参数列表是否相同,基类函数都会被隐藏。

重写(覆盖):是指派生类中存在重新定义的函数。其函数名,参数列表,返回值类型,所有都必须同基类中被重写的函数一致。重写的基类中被重写的函数必须有virtual修饰。
 

重载和重写的区别

(1)范围区别:重写和被重写的函数在不同的类中,重载和被重载的函数在同一类中。

(2)参数区别:重写与被重写的函数参数列表一定相同,重载和被重载的函数参数列表一定不同。

(3)virtual的区别:重写的基类必须要有virtual修饰,重载函数和被重载函数可以被virtual修饰,也可以没有。

隐藏和重写,重载的区别

(1)与重载范围不同:隐藏函数和被隐藏函数在不同类中。

(2)参数的区别:隐藏函数和被隐藏函数参数列表可以相同,也可以不同,但函数名一定同;当参数不同时,无论基类中的函数是否被virtual修饰,基类函数都是被隐藏,而不是被重写。

 

f(int, float),f(float, int),调用f(1, 1)用的哪个?会报错,有多个重载函数 “f” 实例,与参数列表匹配。

 

40、C++ 构造函数和析构函数可以是虚函数嘛?

构造函数不能是虚函数
(1)如果此时构造函数是虚函数,那就要通过对象中的虚函数表指针来调用,而这个虚函数表是在构造函数初始化列表阶段才初始化的。

(2)虚函数主要是实现多态,在运行时才可以明确调用对象,根据传入的对象类型来调用函数。而构造函数是在创建对象时自己主动调用的,不可能通过父类的指针或者引用去调用。那使用虚函数也没有实际意义
  在调用构造函数时还不能确定对象的真实类型(由于子类会调父类的构造函数);并且构造函数的作用是提供初始化,在对象生命期仅仅运行一次,不是对象的动态行为,没有必要成为虚函数。

 

析构函数可以且常常是虚函数

(1)C++类有继承时,析构函数必须为虚函数。如果不是虚函数,则使用时可能存在内存泄漏的问题。

如果我们以这种方式创建对象:

BaseClass* pObj = new SubClass();
delete pObj;

若析构函数不是虚函数(即不加virtual关键词),delete时只释放基类,不释放子类;

 

41、什么是 TCP 半连接队列和全连接队列?

在 TCP 三次握手的时候,Linux 内核会维护两个队列,分别是:

  • 半连接队列,也称 SYN 队列;

  • 全连接队列,也称 accepet 队列;

服务端收到客户端发起的 SYN 请求后,内核会把该连接存储到半连接队列,并向客户端响应 SYN+ACK,接着客户端会返回 ACK,服务端收到第三次握手的 ACK 后,内核会把连接从半连接队列移除,然后创建新的完全的连接,并将其添加到 accept 队列,等待进程调用 accept 函数时把连接取出来。

在这里插入图片描述
不管是半连接队列还是全连接队列,都有最大长度限制,超过限制时,内核会直接丢弃,或返回 RST 包。

 

42、链接失败

在C++环境下使用C函数的时候,常常会出现编译器无法找到obj模块中的C函数定义,从而导致链接失败的情况,应该如何解决这种情况呢?

C++语言在编译的时候为了解决函数的多态问题,会将函数名和参数联合起来生成一个中间的函数名称,而C语言则不会,因此会造成链接时找不到对应函数的情况,此时C函数就需要用extern “C”进行链接指定,这告诉编译器,请保持我的名称,不要给我生成用于链接的中间函数名。
 

43、map和unordered_map的区别

1)头文件不同
map:#include <map>
unordered_map:#include <unordered_map>

 
2) 底层实现的数据结构不同
数据结构其实是两种类型最为根本的区别,其他的不同都是这种区别产生的结果。

map:是基于红黑树结构实现的。红黑树是一种平衡二叉查找树的变体结构。红黑树具有自动排序的功能,因此它使得map也具有按键(key)排序的功能,因此在map中的元素排列都是有序的。在map中,红黑树的每个节点就代表一个元素,因此实现对map的增删改查,相当于对红黑树操作,时间复杂度都为O(logn)。

unordered_map:是基于哈希表(也叫散列表)实现的。散列表是根据关键码值而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。散列表使得unordered_map的插入和查询速度接近于O(1)(在没有冲突的情况下),但是其内部元素的排列顺序是无序的。

 
3)效率及其稳定性不同

  • 存储空间:unordered_map的散列空间会存在部分未被使用的位置,所以其内存效率不是100%的。而map的红黑树的内存效率接近于100%。
  • 查找性能的稳定性:map的查找类似于平衡二叉树的查找,其性能十分稳定。例如在1M数据中查找一个元素,需要20次。而unordered_map依赖于散列表,如果哈希函数映射的关键码出现的冲突过多,则最坏时间复杂度可以达到是O(n)。它的效率是不稳定的。

 
4)优缺点及适用场景
map
优点:

  • map元素有序(这是map最大的优点,其元素的有序性在很多应用中都会简化很多的操作);
  • 其红黑树的结构使得map的很多操作都可在O(logn)下完成;
  • map支持范围查找。

缺点:

  • 占用的空间大:红黑树的每一个节点需要保存其父节点位置、孩子节点位置及红/黑性质,因此每一个节点占用空间大。
  • 查询平均时间不如unordered_map。

适用场景:

  • 元素需要有序;
  • 对于单次查询时间较为敏感,必须保持查询性能的稳定性,比如实时应用等等。

unordered_map
优点:
-查询速度快,平均性能接近于常数时间O(1);

缺点:

  • 元素无序;
  • unordered_map相对于map空间占用更大,且其利用率不高;
  • 查询性能不太稳定,最坏时间复杂度可达到O(n)。

适用场景:

  • 要求查找速率快,且对单次查询性能要求不敏感。

 

44、new的三种用法

第一种:创建对象、调用构造函数 ClassA A=new ClassA();

第二种:是作为修饰符,显示隐藏继承于基类的继承成员
  
第三种:是用在泛型中添加类型的约束,任何类型参数都必须有公共的无参数构造函数。

 

45、vps

即虚拟专用服务器(virtual private server)

VPS是将一台服务器分割成多个虚拟专享服务器的技术。主要分为容器技术和虚拟化技术。在容器或虚拟机中,每个VPS都可分配独立公网IP地址、独立操作系统、实现不同VPS间磁盘空间、内存、CPU资源、进程和系统配置的隔离,为用户和应用程序模拟出“独占”使用计算资源的体验。VPS可以像独立服务器一样,重装操作系统,安装程序,单独重启服务器。VPS为使用者提供了管理配置的自由,企业网站可用VPS主机。

用途
VPS虚拟服务器技术可以通过多种不同的方式灵活地分配服务器资源,每个虚拟化服务器的资源都可以有很大的不同,可以灵活的满足各种高端用户的需求。

1)虚拟主机空间:VPS主机可以像独立服务器一样分割出许多虚拟主机空间,每个空间都可以放许多网站,非常适合为中小企业、小型门户网站、个人工作室。

2)电子商务平台:vps与独立服务器的运行完全相同,中小型服务商可以以较低成本,通过VPS建立自己的电子商务、在线交易平台。

3)ASP应用平台:VPS特有的应用程序模板,可以快速的进行批量部署,再加上独立主机的品质和极低的的成本是中小型企业进行ASP应用的首选平台。

4)数据共享平台:完全的隔离,无与伦比的安全,使得中小企业、专业门户网站可以使用VPS提供数据共享、数据下载服务。对于大型企业来说,可以作为部门级应用平台。

5)数据库存储平台:由于成本比独立服务器低,安全性高,作为小型数据库首选。

6)在线游戏平台:低廉的价格,优秀的品质,独享的资源使得VPS可以作为在线游戏服务器。

 

优缺点
优点:VPS服务器是一种介于传统虚拟主机和独立主机之间的特殊服务器托管技术,它通过特殊的服务器管理技术把一台大型Internet主机虚拟化成多个具有独立IP地址的服务器系统,这些系统无论从性能、安全及扩展性上同独立服务器没有实质性的差别,而费用仅相当于租用独立服务器的1/4或1/5,并且无须额外支出后续的硬件维护管理成本 。

缺陷:由于VPS是在一台独立的服务器上通过VM等虚拟软件虚拟出多个虚拟主机,所以当其中的一台VPS受到攻击或占用大量宽带资源时,其余的VPS也会受到影响。如果因为一台VPS被黑客入侵造成服务器瘫痪,那么其它的VPS也不能工作了。

 

46、数组和指针的区别

数组指针
概念用于存储多个相同类型的集合用于存储变量的地址
赋值数组一个元素一个元素拷贝同类型的指针变量可以相互赋值
存储数组在内存中是连续存放的,开辟一段连续的空间指针可以指向任意类型的数据
sizeofsizeof(数组名) / sizeof(数据类型)32位平台是4,64位平台是8

数组初始化:

1char a[]={"Hello"};//按字符串初始化,大小为6.
2char b[]={'H','e','l','l'};//按字符初始化(错误,输出时将会乱码,没有结束符)
3char c[]={'H','e','l','l','o','\0'};//按字符初始化

指针初始化

// 1)指向对象的指针
int *p=new int(0) ;    delete p;

// 2)指向数组的指针
int *p=new int[n];    delete[] p;

// 3)指向类的指针
Class *p=new Class;  delete p;

// 4)二级指针
int **pp=new (int*)[1]; 
pp[0]=new int[6];
delete[] pp[0];

46.1 数组的创建与销毁

// 一维数组创建与销毁
int* arr = new int[n]; // 创建一维数组
delete[] arr; // 销毁

// 二维数组创建与销毁
int** arr = new int*[row];
for(int i=0;i<row;i++)
{
	arr[i] = new int[col];
}
//释放
for(int i=0;i<row;i++)
{
	delete[] arr[i];
}
delete[] arr;

 

46.2 函数指针、函数指针数组、函数指针数组的指针

void test(const char* str)
{
	printf("%s\n", str);
}
int main()
{
	//函数指针pfun
	void (*pfun)(const char*) = test;
	//函数指针的数组pfunArr
	void (*pfunArr[5])(const char* str);
	pfunArr[0] = test;
	//指向函数指针数组pfunArr的指针ppfunArr
	void (*(*ppfunArr)[10])(const char*) = &pfunArr;
	return 0;
}

 

47、vector和list的区别

1)vector底层实现是数组;list是双向链表。

2)vector支持随机访问;list不支持。

3)vector是顺序内存;list不是。

4)vector在中间节点进行插入删除会导致内存拷贝;list不会。

5)vector一次性分配好内存,不够时才进行2倍扩容;list每次插入新节点都会进行内存申请。

6)vector随机访问性能好,插入删除性能差;list随机访问性能差,插入删除性能好。

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

list拥有一段不连续的内存空间,如果需要高效的插入和删除,而不关心随机访问,则应使用list。

 

48、传值、传指针和传引用

传值:传值无非就是实参拷贝传递给形参,单向传递(实参->形参),赋值完毕后实参就和形参没有任何联系,对形参的修改就不会影响到实参。

传地址:传地址就是把实参的地址复制给形参。复制完毕后实参的地址和形参的地址没有任何联系,对形参地址的修改不会影响到实参, 但是对形参地址所指向对象的修改却直接反应在实参中,因为形参指向的对象就是实参的对象。

传引用:形参相当于是实参的“别名”,对形参的操作其实就是对实参的操作,在引用传递过程中,被调函数的形式参数虽然也作为局部变量在栈中开辟了内存空间,但是这时存放的是由主调函数放进来的实参变量的地址。被调函数对形参的任何操作都被处理成间接寻址,即通过栈中存放的地址访问主调函数中的实参变量。正因为如此,被调函数对形参做的任何操作都影响了主调函数中的实参变量。

 

49、不同cpp文件下全局变量的初始化顺序

对于全局变量的初始化,C语言和C++是有区别的。

在C语言中,只能用常数对全局变量进行初始化,否则编译器会报错。

在C++中,如果在一个文件中定义了 int a = 5;要在另一个文件中定义int b = a 的话,前面必须对a进行声明:extern int a;否则编译不通过。即使是这样,int b = a;这句话也是分两步进行的:在编译阶段,编译器把b当作是未初始化数据而将它初始化为0;在执行阶段,在main被执行前有一个全局对象的构造过程,int b = a; 被当作是int型对象b的拷贝初始化构造来执行。

其实,在C++中全局对象、变量的初始化是独立的,如果不是像int a = 5;这样的已初始化数据,那么就是像b这样的未初始化数据。

而C++中全局对象、变量的构造函数调用顺序是跟声明有一定关系的,即在同一个文件中先声明的先调用。对于不同文件中的全局对象、变量,它们的构造函数调用顺序是未定义的,取决于具体的编译器。

 

50、C++作用域限定符(::)

:: 是运算符中等级最高的,它分为三种:

  • global scope(全局作用域符),用法(::name)。

  • class scope(类作用域符),用法(class::name)。

  • namespace scope(命名空间作用域符),用法(namespace::name)。

都是左关联(left-associativity),作用都是为了更明确的调用想要的变量。

如在程序中的某一处想调用全局变量a,那么就写成::a;如果想调用class A中的成员变量a,那么就写成A::a;如果想调用namespace std中的cout成员,就写成std::cout。

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值