C++常见面试题总结

static关键字

static关键字的作用

保持变量内容持久: static 作用于局部变量,改变了局部变量的生存周期,使得该变量存在于定义后直到程序运行结束的这段时间。
隐藏: static 作用于全局变量和函数,改变了全局变量和函数的作用域,使得全局变量和函数只能在定义它的文件中使用,在源文件中不具有全局可见性。(注:普通全局变量和函数具有全局可见性,即其他的源文件也可以使用。)

  • 修饰全局变量时,表明一个全局变量只对定义在同一文件中的函数可见。其它文件中定义同名变量不会发生冲突。
  • 修饰局部变量时,表明该变量的值不会因为函数终止而丢失。在程序执行到该变量的声明时首次初始化(以后不再初始化,局部变量每次调用都会初始化)。
  • 修饰函数时,表明该函数只在同一文件中调用。其它文件中定义同名函数不会发生冲突。
  • 修饰类的数据成员,表明对该类所有对象这个数据成员都只有一个实例。即该实例归 所有对象共有。类内声明;类外初始化,而且前面不能加static。
  • 用static修饰不访问非静态数据成员、函数的类成员函数。这意味着一个静态成员函数只能访问它的参数、类的静态数据成员和全局变量。

静态变量和非静态变量

  • 全局变量:从定义变量的位置开始到本源文件结束(全局作用域);程序运行期一直存在,程序结束释放;静态区域。程序一启动就会分配存储空间。
  • 静态全局变量:文件作用域(只在被定义的文件中可见),程序运行期一直存在,静态区域。
  • 局部变量:写在函数或代码块中。作用域: 从定义的那一行开始, 一直到遇到大括号或者return(局部作用域)。存储在栈中,系统自动释放。
  • 静态局部变量:局部作用域(只在局部作用域中可见),程序运行期一直存在,静态区域。

静态成员变量

  • 静态成员变量在类内进行声明,在类外进行定义和初始化,在类外进行定义和初始化的时候不要出现 static 关键字和private、public、protected 访问规则。
  • 静态成员变量相当于类域中的全局变量,被类的所有对象所共享,包括派生类的对象。
  • 静态成员变量可以作为成员函数的参数,而普通成员变量不可以。
  • 静态数据成员的类型可以是所属类的类型,而普通数据成员的类型只能是该类类型的指针或引用。

静态函数和非静态函数

静态成员函数和非静态成员函数的根本区别在于有无this指针。 非静态函数由 对象名. 或者 对象指针->调用,调用时编译器会向函数传递this指针;静态成员函数则有类名::或者对象名.调用,没有this指针,不识别对象个体,经常用来操作类的静态数据成员。

类的静态成员(数据成员和函数成员)为类本身所有,在类加载的时候就会分配内存,可以通过类名直接访问,无须创建任何对象或实例就可以访问;非静态成员(数据成员和函数成员)属于类的实例(对象)所有,所以只有在创建类的实例的时候才会分配内存,并通过实例去访问。静态函数只有当程序结束的时候才从内存消失。非静态则是动态加载到内存,不需要的时候就从内存消失。

静态成员函数不能用virtual修饰的原因:

虚函数依靠vptr和vtable来处理。vptr是一个指针,在类的构造函数中创建生成,并且只能用this指针来访问它,因为它是类的一个成员,并且vptr指向保存虚函数地址的vtable。对于静态成员函数,它没有this指针,所以无法访问vptr。因此,static函数不能为virtual。虚函数的调用关系:this -> vptr -> vtable ->virtual function。

静态成员函数不能用const、volatile修饰的原因:

当声明一个非静态成员函数为const时,对this指针会有影响。对于一个Test类中的const修饰的成员函数,this指针相当于Test const , 而对于非const成员函数,this指针相当于Test。而static成员函数没有this指针,所以使用const来修饰static成员函数没有任何意义。volatile的道理也是如此。
静态函数中不能使用非静态变量,也无法访问非静态成员函数,只能调用其他的静态成员函数。静态方法不可以定义this, super关键字(因为静态比对象先加载,而this在创建对象时加载,所以导致冲突,不能使用);非静态函数可以访问静态变量。

static函数的使用场景

普通静态函数只在同一文件中调用,只能在声明它的文件当中可见,与其它文件中定义的同名函数不会冲突。

静态成员函数,为类的全部服务而不是为某一个类的具体对象服务。

成员变量:是在类中声明的,依类而生,离开类之后就不是成员变量。成员变量只能通过对象访问。存储在栈中。静态成员变量类内声明,类外初始化。

C 和 C++ static 的区别

  • C语言中使用 static 可以定义局部静态变量、外部静态变量、静态函数。
  • C++ 中使用 static 可以定义局部静态变量、外部静态变量、静态函数、静态成员变量和静态成员函数。因为 C++ 中有类的概念,静态成员变量、静态成员函数都是与类有关的概念。

new和malloc

new 是 C++ 中的关键字,用来动态分配内存空间,实现方式如下:

int *p = new int[5];

new和malloc的区别

  • 属性:new/delete是C++关键字,需要编译器支持。malloc/free是库函数,需要头文件支持。
  • 参数:使用new操作符申请内存时无须指定内存块的大小,编译器会根据类型信息自行计算。而malloc则需要显式地指出所需内存的尺寸。
  • 返回类型:new操作符内存分配成功时,返回的是对象类型的指针,无须进行类型转换,故new是符合类型安全性的操作符。而malloc内存分配成功则是返回void * ,需要通过强制类型转换将 void* 指针转换成我们需要的类型。
  • 分配失败:new内存分配失败时,会抛出bac_alloc异常(分配成功则返回该对象类型的指针)。malloc分配内存失败时返回NULL(成功申请到内存则返回指向该内存的指针,void * 类型)。
  • 自定义类型:new会先调用operator new函数,申请足够的内存(通常底层使用malloc实现)。然后调用类型的构造函数,初始化成员变量,最后返回自定义类型指针。delete先调用析构函数,然后调用operator delete函数释放内存(通常底层使用free实现)。malloc/free是库函数,只能动态的申请和释放内存,无法强制要求其做自定义类型对象构造和析构工作。
  • 重载:C++允许重载new/delete操作符;malloc不允许重载。
  • 内存泄漏:对于new和malloc都能检测出来,而new可以指明是哪个文件的哪一行,malloc确不可以。
  • 内存区域:new操作符从自由存储区(free store)上为对象动态分配内存空间,而malloc函数从堆上动态分配内存。自由存储区是C++基于new操作符的一个抽象概念,凡是通过new操作符进行内存申请,该内存即为自由存储区。而堆是操作系统中的术语,是操作系统所维护的一块特殊内存,用于程序的内存动态分配。C语言使用malloc从堆上分配内存,使用free释放已分配的对应内存。自由存储区不等于堆,布局new就可以不位于堆中。

内存泄漏

指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况(如只new不delete)。它并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。

检测可行的办法:在申请内存时记录下该内存的地址和在代码中申请内存的位置,在内存销毁时删除该地址对应的记录,程序最后统计下还有哪条记录没有被删除,如果还有没被删除的记录就代表有内存泄漏。

malloc的原理和底层实现

malloc的原理:

  • 当开辟的空间小于128K时,调用brk()函数,通过移动_enddata来实现;
  • 当开辟空间大于128K时,调用mmap()函数,通过在虚拟地址空间中开辟一块内存空间来实现。

malloc的底层实现:
brk()函数实现原理: 向高地址的方向移动指向数据段的高地址的指针 _enddata。
mmap内存映射原理:

  • 进程启动映射过程,并在虚拟地址空间中为映射创建虚拟映射区域;
  • 调用内核空间的系统调用函数 mmap(),实现文件物理地址和进程虚拟地址的一一映射关系;
  • 进程发起对这片映射空间的访问,引发缺页异常,实现文件内容到物理内存(主存)的拷贝。

malloc如何实现的线程安全

malloc函数线程安全但是不可重入的。

在拥有共享数据的多条线程并行执行的程序中,线程安全的代码会通过同步机制保证各个线程都可以正常且正确的执行,不会出现数据污染等意外情况。一个函数要做到线程安全,需要解决多个线程调用函数时访问共享资源的冲突。

可重入:在运行某函数或代码时因为某个原因(中断或者抢占资源问题)而中止函数或代码的运行,等到问题解决后,重新进入该函数或者代码继续运行。其结果不会受到影响(和没有被打断时,运行结果一样)。要做到可重入,需要不在函数内部使用静态或全局数据,不返回静态或全局数据,也不调用不可重入函数。

malloc函数在用户空间要自己管理各进程共享的内存链表,由于有共享资源访问,本身会造成线程不安全。为了做到线程安全,需要加锁进行保护。同时这个锁必须是递归锁,因为如果当程序调用malloc函数时收到信号,在信号处理函数里再调用malloc函数,如果使用一般的锁就会造成死锁(信号处理函数中断了原程序的执行),所以要使用递归锁。

虽然使用递归锁能够保证malloc函数的线程安全性,但是不能保证它的可重入性。按上面的场景,程序调用malloc函数时收到信号,在信号处理函数里再调用malloc函数就可能破坏共享的内存链表等资源,因而是不可重入的。

至于malloc函数访问内核的共享数据结构可以正常的加锁保护,因为一个进程程调用malloc函数进入内核时,必须等到返回用户空间前夕才能执行信号处理函数,这时内核数据结构已经访问完成,内核锁已释放,所以不会有问题。

malloc函数用法

void *malloc(long NumBytes):该函数分配了NumBytes个字节,并返回了指向这块内存的指针。如果分配失败,则返回一个空指针(NULL)。
char *Ptr = NULL;
Ptr = (char *)malloc(100 * sizeof(char));
void free(void *FirstByte):该函数是将之前用malloc分配的空间还给程序或者是操作系统,也就是释放了这块内存,让它重新得到自由。
free(Ptr);
Ptr = NULL;

delete 和 free

在使用的时候 new、delete 搭配使用,malloc、free 搭配使用。 malloc、free 是库函数,而new、delete 是关键字。

delete 的实现原理

首先执行该对象所属类的析构函数;
进而通过调用 operator delete 的标准库函数来释放所占的内存空间。

delete 和 delete [] 的区别

delete 用来释放单个对象所占的空间,只会调用一次析构函数;
delete [] 用来释放数组空间,会对数组中的每个成员都调用一次析构函数。

const

const关键字的作用

  • const 修饰成员变量,定义成 const 常量,相较于宏常量,可进行类型检查,节省内存空间,提高了效率。
  • const 修饰函数参数,使得传递过来的函数参数的值不能改变。
  • const修饰成员函数,使成员函数不能修改任何类型的成员变量(mutable 修饰的变量除外),也不能调用非 const 成员函数,因为非 const 成员函数可能会修改成员变量。

const在类中的作用

const 成员变量:

  • const成员变量只能在类内声明、定义,在构造函数初始化列表中初始化。
  • const 成员变量只在某个对象的生存周期内是常量,对于整个类而言却是可变的,因为类可以创建多个对象,不同类的 const 成员变量的值是不同的。因此不能在类的声明中初始化 const 成员变量,类的对象还没有创建,编译器不知道它的值。

const 成员函数:

  • 不能修改成员变量的值,除非有 mutable 修饰;只能访问成员变量。
  • 不能调用非常量成员函数,以防修改成员变量的值。

struct

C 和 C++ struct 的区别?

  • C 语言中 struct 是用户自定义数据类型;在 C++ 中 struct 是抽象数据类型,支持成员函数的定义。
  • C 语言中 struct 没有访问权限的设置,是一些变量的集合体,不能定义成员函数;C++ 中 struct 可以和类一样,有访问权限,并可以定义成员函数。
  • C 语言中 struct 定义的自定义数据类型,在定义该类型的变量时,需要加上 struct 关键字,例如:struct A var;,定义 A 类型的变量;而 C++ 中,不用加该关键字,例如:A var;。

为什么有了class还保留struct?

C++ 是在 C 语言的基础上发展起来的,为了与 C 语言兼容,C++ 中保留了 struct。

class 和 struct 的异同?

  • struct 和 class 都可以自定义数据类型,也支持继承操作。
  • struct 中默认的访问级别是public,默认的继承级别也是public;class中默认的访问级别是 private,默认的继承级别也是 private。
  • 当 class 继承 struct 或者 struct 继承 class 时,默认的继承级别取决于 class 或 struct 本身, class(private 继承),struct(public 继承),即取决于派生类的默认继承级别。
struct A{}class B : A{}; // private 继承 
struct C : B{}// public 继承
  • class 可以用于定义模板参数,struct 不能用于定义模板参数。

struct 和 union 的区别?

union 是联合体,struct 是结构体。

  • 联合体和结构体都是由若干个数据类型不同的数据成员组成。使用时,联合体只有一个有效的成员;而结构体所有的成员都有效。
  • 对联合体的不同成员赋值,将会覆盖其他成员的值;而对结构体的不同成员赋值时,相互不影响。
  • 联合体的大小为其内部所有变量的最大值,按照最大类型的倍数进行分配大小;结构体分配内存的大小遵循内存对齐原则。
typedef union
{
    char c[10];
    char cc1; // char 1 字节,按该类型的倍数分配大小
} u11;
typedef union
{
    char c[10];
    int i; // int 4 字节,按该类型的倍数分配大小
} u22;
typedef union
{
    char c[10];
    double d; // double 8 字节,按该类型的倍数分配大小
} u33;

typedef struct s1
{
    char c;   // 1 字节
    double d; // 1(char)+ 7(内存对齐)+ 8(double)= 16 字节
} s11;
typedef struct s2
{
    char c;   // 1 字节
    char cc;  // 1(char)+ 1(char)= 2 字节
    double d; // 2 + 6(内存对齐)+ 8(double)= 16 字节
} s22;
typedef struct s3
{
    char c;   // 1 字节
    double d; // 1(char)+ 7(内存对齐)+ 8(double)= 16 字节
    char cc;  // 16 + 1(char)+ 7(内存对齐)= 24 字节
} s33;

int main()
{
    cout << sizeof(u11) << endl; // 10
    cout << sizeof(u22) << endl; // 12
    cout << sizeof(u33) << endl; // 16
    cout << sizeof(s11) << endl; // 16
    cout << sizeof(s22) << endl; // 16
    cout << sizeof(s33) << endl; // 24
    cout << sizeof(int) << endl;    // 4
    cout << sizeof(double) << endl; // 8
    return 0;
}

volatile

volatile提醒编译器它后面所定义的变量随时都有可能改变,因此编译后的程序每次需要存储或读取这个变量的时候,都会直接从变量地址中读取数据。如果没有volatile关键字,则编译器可能优化读取和存储,可能暂时使用寄存器中的值,如果这个变量由别的程序更新了的话,将出现不一致的现象。

volatile 的作用:当对象的值可能在程序的控制或检测之外被改变时,应该将该对象声明为 volatile,告知编译器不应对这样的对象进行优化。

volatile不具有原子性。(实现原子操作用automic,版本:C++11及以上)

volatile 对编译器的影响:使用该关键字后,编译器不会对相应的对象进行优化,即不会将变量从内存缓存到寄存器中,防止多个线程有可能使用内存中的变量,有可能使用寄存器中的变量,从而导致程序错误。

volatile关键字的使用场景? 能否和const一起使用?

  • 当多个线程都会用到某一变量,并且该变量的值有可能发生改变时,需要用 volatile 关键字对该变量进行修饰;
  • 中断服务程序中访问的变量或并行设备的硬件寄存器的变量,最好用 volatile 关键字修饰。

volatile 关键字和 const 关键字可以同时使用,某种类型可以既是 volatile 又是 const ,同时具有二者的属性。

sizeof 和 strlen 的区别

  • strlen 是头文件 中的函数;sizeof 是 C++ 中的运算符。
  • strlen 测量的是字符串的实际长度(其源代码如下),以 \0 结束。而 sizeof 测量的是字符数组的分配大小。
char arr[10] = "hello";
cout << strlen(arr) << endl; // 5
cout << sizeof(arr) << endl; // 10
  • 若字符数组 arr 作为函数的形参,sizeof(arr) 中 arr 被当作字符指针来处理,strlen(arr) 中 arr 依然是字符数组。
void size_of(char arr[])
{
    cout << sizeof(arr) << endl; // warning: 'sizeof' on array function parameter 'arr' will return size of 'char*' .
    cout << strlen(arr) << endl; 
}
int main()
{
    char arr[20] = "hello";
    size_of(arr); 
    return 0;
}
//输出结果:8 ;5
  • strlen 本身是库函数,因此在程序运行过程中,计算长度;而 sizeof 在编译时,计算长度;
  • sizeof 的参数可以是类型,也可以是变量;strlen 的参数必须是 char* 类型的变量。

lambda 表达式

lambda 表达式的定义形式如下:

[capture list] (parameter list) -> reurn type
{
   function body
}

其中:

  • capture list:捕获列表,指 lambda 表达式所在函数中定义的局部变量的列表,通常为空,但如果函数体中用到了 lambda 表达式所在函数的局部变量,必须捕获该变量,即将此变量写在捕获列表中。捕获方式分为:引用捕获方式 [&]、值捕获方式 [=]。
  • return type、parameter list、function body:分别表示返回值类型、参数列表、函数体,和普通函数一样。

lambda 表达式常搭配排序算法使用。

vector<int> arr = {3, 4, 76, 12, 54, 90, 34};
sort(arr.begin(), arr.end(), [](int a, int b) { return a > b; }); // 降序排序
for (auto a : arr)
{
    cout << a << " ";
}
//运行结果:90 76 54 34 12 4 3

explicit

作用:用来声明类构造函数是显示调用的,而非隐式调用,可以阻止调用构造函数时进行隐式转换。只可用于修饰单参构造函数,因为无参构造函数和多参构造函数本身就是显示调用的,再加上 explicit 关键字也没有什么意义。

class A
{
public:
    int var;
    A(int tmp)
    {
        var = tmp;
    }
};
int main()
{
    A ex = 10; // 发生了隐式转换
    return 0;
}

上述代码中,A ex = 10; 在编译时,进行了隐式转换,将 10 转换成 A 类型的对象,然后将该对象赋值给 ex。为了避免隐式转换,可用 explicit 关键字进行声明:

class A
{
public:
    int var;
    explicit A(int tmp)
    {
        var = tmp;
        cout << var << endl;
    }
};
int main()
{
    A ex(100);
    A ex1 = 10; // error: conversion from 'int' to non-scalar type 'A' requested
    return 0;
}

define

define 和 const 的区别

  • 编译阶段:define 是在编译预处理阶段进行替换,const 是在编译阶段确定其值。
  • 安全性:define 定义的宏常量没有数据类型,只是进行简单的替换,不会进行类型安全的检查;const 定义的常量是有类型的,是要进行判断的,可以避免一些低级的错误。
  • 内存占用:define 定义的宏常量,在程序中使用多少次就会进行多少次替换,内存中有多个备份,占用的是代码段的空间;const 定义的常量占用静态存储区的空间,程序运行过程中只有一份。
  • 调试:define 定义的宏常量不能调试,因为在预编译阶段就已经进行替换了;const 定义的常量可以进行调试。

const 的优点:

  • 有数据类型,在定义式可进行安全性检查。
  • 可调式。
  • 占用较少的空间。

define 和 typedef 的区别

  • 原理:#define 作为预处理指令,在编译预处理时进行替换操作,不作正确性检查,只有在编译已被展开的源程序时才会发现可能的错误并报错。typedef 是关键字,在编译时处理,有类型检查功能,用来给一个已经存在的类型一个别名,但不能在一个函数定义里面使用 typedef 。
  • 功能:typedef 用来定义类型的别名,方便使用。#define 不仅可以为类型取别名,还可以定义常量、变量、编译开关等。
  • 作用域:#define 没有作用域的限制,只要是之前预定义过的宏,在以后的程序中都可以使用,而 typedef 有自己的作用域。
  • 指针的操作:typedef 和 #define 在处理指针时不完全一样
#define INTPTR1 int *
typedef int * INTPTR2;
INTPTR1 p1, p2; // p1: int *; p2: int
INTPTR2 p3, p4; // p3: int *; p4: int *
int var = 1;
const INTPTR1 p5 = &var; // 相当于 const int * p5; 常量指针,即不可以通过 p5 去修改 p5 指向的内容,但是 p5 可以指向其他内容。
const INTPTR2 p6 = &var; // 相当于 int * const p6; 指针常量,不可使 p6 再指向其他内容。

用宏实现比较大小,以及两个数中的最小值

#define MAX(X, Y) ((X)>(Y)?(X):(Y))
#define MIN(X, Y) ((X)<(Y)?(X):(Y))
int var1 = 10, var2 = 100;
cout << MAX(var1, var2) << endl;
cout << MIN(var1, var2) << endl;
//程序运行结果:100 ;10

inline

inline作用

inline 是一个关键字,可以用于定义内联函数。内联函数,像普通函数一样被调用,但是在调用时并不通过函数调用的机制而是直接在调用点处展开,这样可以大大减少由函数调用带来的开销,从而提高程序的运行效率。

inline使用方法

  • 类内定义成员函数默认是内联函数。
    在类内定义成员函数,可以不在函数头部加inline关键字,因为编译器会自动将类内定义的函数(构造函数、析构函数、普通成员函数等)声明为内联函数。
  • 类外定义成员函数,若想定义为内联函数,需用关键字声明。
    在类内声明函数,在类外定义函数时,如果想将该函数定义为内联函数,则可以在类内声明时不加inline关键字,而在类外定义函数时加上inline关键字。
  • 可以在声明函数和定义函数的同时加上 inline;也可以只在函数声明时加 inline,而定义函数时不加 inline。只要确保在调用该函数之前把 inline 的信息告知编译器即可。

inline函数工作原理

  • 内联函数不是在调用时发生控制转移关系,而是在编译阶段将函数体嵌入到每一个调用该函数的语句块中,编译器会将程序中出现内联函数的调用表达式用内联函数的函数体来替换。
  • 普通函数是将程序执行转移到被调用函数所存放的内存地址,当函数执行完后,返回到执行此函数前的地方。转移操作需要保护现场,被调函数执行完后,再恢复现场,该过程需要较大的资源开销。

宏定义(define)和内联函数(inline)的区别

  • 内联函数是在编译时展开,而宏在编译预处理时展开;在编译的时候,内联函数直接被嵌入到目标代码中去,而宏只是一个简单的文本替换。
  • 内联函数是真正的函数,和普通函数调用的方法一样,在调用点处直接展开,避免了函数的参数压栈操作,减少了调用的开销。而宏定义编写较为复杂,常需要增加一些括号来避免歧义。
  • 宏定义只进行文本替换,不会对参数的类型、语句能否正常编译等进行检查。而内联函数是真正的函数,会对参数的类型、函数体内的语句编写是否正确等进行检查。

定义和声明的区别

定义直接告诉你了所有的东西,这个变量是什么,这个函数是什么功能,这个类里面包含了什么东西。
声明就是指给除了当前变量或者函数,或者类什么的名字,不给其中的内容,就是先告诉你有这样一个什么类型的变量或者函数,但是这个变量或者函数的具体信息却是不知道的。

对于变量来说

定义:可以为变量分配存储空间,并且可以给变量一个初始值。
声明:告诉编译器这个变量的名字和类型(extern int a;(在没有赋值的情况下,变量前加上关键字extern一定为声明))。

对于函数来说

定义:就是这个函数具体的实现
声明:告诉编译器在这个程序中会有这么一个函数
简单来说,如果函数带有{},则其为定义;否则,就为声明。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值