C++面试常见问题

指针和引用的区别

  • 指针是一个变量,存储的是一个地址,引用跟原来的变量实质上是同一个东西,是原变量的别名
  • 指针可以有多级,引用只有一级
  • 指针可以为空,引用不能为NULL且在定义时必须初始化
  • 指针在初始化后可以改变指向,而引用在初始化之后不可再改变
  • sizeof指针得到的是本指针的大小,sizeof引用得到的是引用所指向变量的大小
  • 当把指针作为参数进行传递时,也是将实参的一个拷贝传递给形参,两者指向的地址相同,但不是同一个变量,在函数中改变这个变量的指向不影响实参,而引用却可以。
  • 引用只是别名,不占用具体存储空间,只有声明没有定义;指针是具体变量,需要占用存储空间。
  • 引用在声明时必须初始化为另一变量,一旦出现必须为typename refname &varname形式;指针声明和定义可以分开,可以先只声明指针变量而不初始化,等用到时再指向具体变量。
  • 引用一旦初始化之后就不可以再改变(变量可以被引用为多次,但引用只能作为一个变量引用);指针变量可以重新指向别的变量。
  • 不存在指向空值的引用,必须有具体实体;但是存在指向空值的指针。

堆和栈的区别

  • 申请方式不同:栈由系统自动分配;堆是自己申请和释放的。
  • 申请大小限制不同:栈顶和栈底是之前预设好的,栈是向栈底扩展,大小固定,可以通过ulimit -a查看,由ulimit -s修改;堆向高地址扩展,是不连续的内存区域,大小可以灵活调整。
  • 申请效率不同:栈由系统分配,速度快,不会有碎片;堆由程序员分配,速度慢,且会有碎片。

区别以下指针类型

int *p[10]
int (*p)[10]
int *p(int)
int (*p)(int)
  • int *p[10]表示指针数组,强调数组概念,是一个数组变量,数组大小为10,数组内每个元素都是指向int类型的指针变量。
  • int (*p)[10]表示数组指针,强调是指针,只有一个变量,是指针类型,不过指向的是一个int类型的数组,这个数组大小是10。
  • int *p(int)是函数声明,函数名是p,参数是int类型的,返回值是int *类型的。
  • int (*p)(int)是函数指针,强调是指针,该指针指向的函数具有int类型参数,并且返回值是int类型的。

new / delete 与 malloc / free的异同

相同点

  • 都可用于内存的动态申请和释放

不同点

  • 前者是C++运算符,后者是C/C++语言标准库函数
  • new自动计算要分配的空间大小,malloc需要手工计算
  • new是类型安全的,malloc不是。例如:
int *p = new float[2]; //编译错误
int *p = (int*)malloc(2 * sizeof(double));//编译无错误
  • new调用名为operator new的标准库函数分配足够空间并调用相关对象的构造函数,delete对指针所指对象运行适当的析构函数;然后通过调用名为operator delete的标准库函数释放该对象所用内存。后者均没有相关调用
  • 后者需要库文件支持,前者不用
  • new是封装了malloc,直接free不会报错,但是这只是释放内存,而不会析构对象

宏定义和typedef区别?

  • 宏主要用于定义常量及书写复杂的内容;typedef主要用于定义类型别名。
  • 宏替换发生在编译阶段之前,属于文本插入替换;typedef是编译的一部分。
  • 宏不检查类型;typedef会检查数据类型。
  • 宏不是语句,不在在最后加分号;typedef是语句,要加分号标识结束。
  • 注意对指针的操作,typedef char * p_char和#define p_char char *区别巨大。

strlen和sizeof区别?

  • sizeof是运算符,并不是函数,结果在编译时得到而非运行中获得;strlen是字符处理的库函数。
  • sizeof参数可以是任何数据的类型或者数据(sizeof参数不退化);strlen的参数只能是字符指针且结尾是'\0'的字符串。
  • 因为sizeof值在编译时确定,所以不能用来得到动态分配(运行时分配)存储空间的大小。
  int main(int argc, char const *argv[]){   
      const char* str = "name";
      sizeof(str); // 取的是指针str的长度,是8
      strlen(str); // 取的是这个字符串的长度,不包含结尾的 \0。大小是4
      return 0;
  }

常量指针和指针常量区别?

  • 常量指针是一个指针,读成常量的指针,指向一个只读变量。如int const *p或const int *p。
  • 指针常量是一个不能给改变指向的指针。指针是个常量,不能中途改变指向,如int *const p。

a和&a有什么区别?

假设数组int a[10];
int (*p)[10] = &a;
  • a是数组名,是数组首元素地址,+1表示地址值加上一个int类型的大小,如果a的值是0x00000001,加1操作后变为0x00000005。*(a + 1) = a[1]。
  • &a是数组的指针,其类型为int (*)[10](就是前面提到的数组指针),其加1时,系统会认为是数组首地址加上整个数组的偏移(10个int型变量),值为数组a尾元素后一个元素的地址。
  • 若(int *)p ,此时输出 *p时,其值为a[0]的值,因为被转为int *类型,解引用时按照int类型大小来读取。

C++中struct和class的区别

相同点

  • 两者都拥有成员函数、公有和私有部分
  • 任何可以使用class完成的工作,同样可以使用struct完成

不同点

  • 两者中如果不对成员不指定公私有,struct默认是公有的,class则默认是私有的
  • class默认是private继承,而struct模式是public继承
  • class可以作为模板类型,struct不行

引申:C++和C的struct区别

  • C语言中:struct是用户自定义数据类型(UDT);C++中struct是抽象数据类型(ADT),支持成员函数的定义,(C++中的struct能继承,能实现多态)
  • C中struct是没有权限的设置的,且struct中只能是一些变量的集合体,可以封装数据却不可以隐藏数据,而且成员不可以是函数
  • C++中,struct增加了访问权限,且可以和类一样有成员函数,成员默认访问说明符为public(为了与C兼容)
  • struct作为类的一种特例是用来自定义数据结构的。一个结构标记声明后,在C中必须在结构标记前加上struct,才能做结构类型名(除:typedef struct class{};);C++中结构体标记(结构体名)可以直接作为结构体类型名使用,此外结构体struct在C++中被当作类的一种特例

define宏定义和const的区别

编译阶段

  • define是在编译的预处理阶段起作用,而const是在编译、运行的时候起作用

安全性

  • define只做替换,不做类型检查和计算,也不求解,容易产生错误,一般最好加上一个大括号包含住全部的内容,要不然很容易出错
  • const常量有数据类型,编译器可以对其进行类型安全检查

内存占用

  • define只是将宏名称进行替换,在内存中会产生多分相同的备份。const在程序运行中只有一份备份,且可以执行常量折叠,能将复杂的的表达式计算出结果放入常量表
  • 宏替换发生在编译阶段之前,属于文本插入替换;const作用发生于编译过程中。
  • 宏不检查类型;const会检查数据类型。
  • 宏定义的数据没有分配内存空间,只是插入替换掉;const定义的变量只是值不能改变,但要分配内存空间。

C++的顶层const和底层const

概念区分

  • 顶层const:指的是const修饰的变量本身是一个常量,无法修改,指的是指针,就是 * 号的右边
  • 底层const:指的是const修饰的变量所指向的对象是一个常量,指的是所指变量,就是 * 号的左边

举个例子

int a = 10;
int* const b1 = &a;        //顶层const,b1本身是一个常量
const int* b2 = &a;        //底层const,b2本身可变,所指的对象是常量
const int b3 = 20;         //顶层const,b3是常量不可变
const int* const b4 = &a;  //前一个const为底层,后一个为顶层,b4不可变
const int& b5 = a;         //用于声明引用变量,都是底层const

区分作用

  • 执行对象拷贝时有限制,常量的底层const不能赋值给非常量的底层const
  • 使用命名的强制类型转换函数const_cast时,只能改变运算对象的底层const

拷贝初始化和直接初始化

  • 当用于类类型对象时,初始化的拷贝形式和直接形式有所不同:直接初始化直接调用与实参匹配的构造函数,拷贝初始化总是调用拷贝构造函数。拷贝初始化首先使用指定构造函数创建一个临时对象,然后用拷贝构造函数将那个临时对象拷贝到正在创建的对象。举例如下
string str1("I am a string");//语句1 直接初始化
string str2(str1);//语句2 直接初始化,str1是已经存在的对象,直接调用构造函数对str2进行初始化
string str3 = "I am a string";//语句3 拷贝初始化,先为字符串”I am a string“创建临时对象,再把临时对象作为参数,使用拷贝构造函数构造str3
string str4 = str1;//语句4 拷贝初始化,这里相当于隐式调用拷贝构造函数,而不是调用赋值运算符函数
  • 为了提高效率,允许编译器==跳过创建临时对象这一步,直接调用构造函数构造要创建的对象,这样就完全等价于直接初始化了(语句1和语句3等价)。但是需要辨别两种情况。
    • 当拷贝构造函数为private时:语句3和语句4在编译时会报错
    • 使用explicit修饰构造函数时:如果构造函数存在隐式转换,编译时会报错

内联函数和宏定义的区别

内联(inline)函数和普通函数相比可以加快程序运行的速度,因为不需要中断调用,在编译的时候内联函数可以直接嵌入到目标代码中。

内联函数适用场景

  • 使用宏定义的地方都可以使用inline函数
  • 作为类成员接口函数来读写类的私有成员或者保护成员,会提高效率

为什么不能把所有的函数写成内联函数

内联函数以代码复杂为代价,它以省去函数调用的开销来提高执行效率。所以一方面如果内联函数体内代码执行时间相比函数调用开销较大,则没有太大的意义;另一方面每一处内联函数的调用都要复制代码,消耗更多的内存空间,因此以下情况不宜使用内联函数:

  • 函数体内的代码比较长,将导致内存消耗代价
  • 函数体内有循环,函数执行时间要比函数调用开销大

主要区别

  • 内联函数在编译时展开,宏在预编译时展开

  • 内联函数直接嵌入到目标代码中,宏是简单的做文本替换

  • 内联函数有类型检测、语法判断等功能,而宏没有

  • 内联函数是函数,宏不是

  • 宏定义时要注意书写(参数要括起来)否则容易出现歧义,内联函数不会产生歧义

  • 内联函数代码是被放到符号表中,使用时像宏一样展开,没有调用的开销,效率很高;

  • 在使用时,宏只做简单字符串替换(编译前)。而内联函数可以进行参数类型检查(编译时),且具有返回值。

  • 内联函数本身是函数,强调函数特性,具有重载等功能。

  • 内联函数可以作为某个类的成员函数,这样可以使用类的保护成员和私有成员,进而提升效率。而当一个表达式涉及到类保护成员或私有成员时,宏就不能实现了。

构造函数、析构函数、虚函数可否声明为内联函数

首先,将这些函数声明为内联函数,在语法上没有错误。因为inline同register一样,只是个建议,编译器并不一定真正的内联。

register关键字:这个关键字请求编译器尽可能的将变量存在CPU内部寄存器中,而不是通过内存寻址访问,以提高效率

举个例子:

#include <iostream>
using namespace std;
class A
{
public:
    inline A() {
        cout << "inline construct()" <<endl;
    }
    inline ~A() {
        cout << "inline destruct()" <<endl;
    }
    inline virtual void  virtualFun() {
        cout << "inline virtual function" <<endl;
    }
};

int main()
{
    A a;
    a.virtualFun();
    return 0;
}
//输出结果
//inline construct()
//inline virtual function
//inline destruct()

构造函数和析构函数声明为内联函数是没有意义的

《Effective C++》中所阐述的是:将构造函数和析构函数声明为inline是没有什么意义的,即编译器并不真正对声明为inline的构造和析构函数进行内联操作,因为编译器会在构造和析构函数中添加额外的操作(申请/释放内存,构造/析构对象等),致使构造函数/析构函数并不像看上去的那么精简。其次,class中的函数默认是inline型的,编译器也只是有选择性的inline,将构造函数和析构函数声明为内联函数是没有什么意义的。

将虚函数声明为inline,要分情况讨论

有的人认为虚函数被声明为inline,但是编译器并没有对其内联,他们给出的理由是inline是编译期决定的,而虚函数是运行期决定的,即在不知道将要调用哪个函数的情况下,如何将函数内联呢?

上述观点看似正确,其实不然,如果虚函数在编译器就能够决定将要调用哪个函数时,就能够内联,那么什么情况下编译器可以确定要调用哪个函数呢,答案是当用对象调用虚函数(此时不具有多态性)时,就内联展开

综上,当是指向派生类的指针(多态性)调用声明为inline的虚函数时,不会内联展开;当是对象本身调用虚函数时,会内联展开,当然前提依然是函数并不复杂的情况下

auto、decltype和decltype(auto)的用法

auto

C++11新标准引入了auto类型说明符,用它就能让编译器替我们去分析表达式所属的类型。和原来那些只对应某种特定的类型说明符(例如 int)不同,

auto 让编译器通过初始值来进行类型推演。从而获得定义变量的类型,所以说auto 定义的变量必须有初始值。举个例子:

//普通;类型
int a = 1, b = 3;
auto c = a + b;// c为int型

//const类型
const int i = 5;
auto j = i; // 变量i是顶层const, 会被忽略, 所以j的类型是int
auto k = &i; // 变量i是一个常量, 对常量取地址是一种底层const, 所以b的类型是const int*
const auto l = i; //如果希望推断出的类型是顶层const的, 那么就需要在auto前面加上cosnt

//引用和指针类型
int x = 2;
int& y = x;
auto z = y; //z是int型不是int& 型
auto& p1 = y; //p1是int&型
auto p2 = &x; //p2是指针类型int*

decltype

有的时候我们还会遇到这种情况,我们希望从表达式中推断出要定义变量的类型,但却不想用表达式的值去初始化变量。还有可能是函数的返回类型为某表达式的值类型。在这些时候auto显得就无力了,所以C++11又引入了第二种类型说明符decltype,它的作用是选择并返回操作数的数据类型。在此过程中,编译器只是分析表达式并得到它的类型,却不进行实际的计算表达式的值。

int func() {return 0};

//普通类型
decltype(func()) sum = 5; // sum的类型是函数func()的返回值的类型int, 但是这时不会实际调用函数func()
int a = 0;
decltype(a) b = 4; // a的类型是int, 所以b的类型也是int

//不论是顶层const还是底层const, decltype都会保留   
const int c = 3;
decltype(c) d = c; // d的类型和c是一样的, 都是顶层const
int e = 4;
const int* f = &e; // f是底层const
decltype(f) g = f; // g也是底层const

//引用与指针类型
//1. 如果表达式是引用类型, 那么decltype的类型也是引用
const int i = 3, &j = i;
decltype(j) k = 5; // k的类型是 const int&

//2. 如果表达式是引用类型, 但是想要得到这个引用所指向的类型, 需要修改表达式:
int i = 3, &r = i;
decltype(r + 0) t = 5; // 此时是int类型

//3. 对指针的解引用操作返回的是引用类型
int i = 3, j = 6, *p = &i;
decltype(*p) c = j; // c是int&类型, c和j绑定在一起

//4. 如果一个表达式的类型不是引用, 但是我们需要推断出引用, 那么可以加上一对括号, 就变成了引用类型了
int i = 3;
decltype((i)) j = i; // 此时j的类型是int&类型, j和i绑定在了一起

decltype(auto)

decltype(auto)是C++14新增的类型指示符,可以用来声明变量以及指示函数返回类型。在使用时,会将“=”号左边的表达式替换掉auto,再根据decltype的语法规则来确定类型。举个例子:

int e = 4;
const int* f = &e; // f是底层const
decltype(auto) j = f;//j的类型是const int* 并且指向的是e

volatile、mutable和explicit关键字的用法

volatile

volatile 关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素更改,比如:操作系统、硬件或者其它线程等。遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问。

当要求使用 volatile 声明的变量的值的时候,系统总是重新从它所在的内存读取数据,即使它前面的指令刚刚从该处读取过数据。

volatile定义变量的值是易变的,每次用到这个变量的值的时候都要去重新读取这个变量的值,而不是读寄存器内的备份。多线程中被几个任务共享的变量需要定义为volatile类型。

volatile 指针

volatile 指针和 const 修饰词类似,const 有常量指针和指针常量的说法,volatile 也有相应的概念

修饰由指针指向的对象、数据是 const 或 volatile 的:

const char* cpch;
volatile char* vpch;

指针自身的值——一个代表地址的整数变量,是 const 或 volatile 的:

char* const pchc;
char* volatile pchv;

注意:

  • 可以把一个非volatile int赋给volatile int,但是不能把非volatile对象赋给一个volatile对象。
  • 除了基本类型外,对用户定义类型也可以用volatile类型进行修饰。
  • C++中一个有volatile标识符的类只能访问它接口的子集,一个由类的实现者控制的子集。用户只能用const_cast来获得对类型接口的完全访问。此外,volatile向const一样会从类传递到它的成员。

多线程下的volatile

有些变量是用volatile关键字声明的。当两个线程都要用到某一个变量且该变量的值会被改变时,应该用volatile声明,该关键字的作用是防止优化编译器把变量从内存装入CPU寄存器中。如果变量被装入寄存器,那么两个线程有可能一个使用内存中的变量,一个使用寄存器中的变量,这会造成程序的错误执行。volatile的意思是让编译器每次操作该变量时一定要从内存中真正取出,而不是使用已经存在寄存器中的值。

mutable

mutable的中文意思是“可变的,易变的”,跟constant(既C++中的const)是反义词。在C++中,mutable也是为了突破const的限制而设置的。被mutable修饰的变量,将永远处于可变的状态,即使在一个const函数中。我们知道,如果类的成员函数不会改变对象的状态,那么这个成员函数一般会声明成const的。但是,有些时候,我们需要在const函数里面修改一些跟类状态无关的数据成员,那么这个函数就应该被mutable来修饰,并且放在函数后后面关键字位置。

explicit

explicit关键字用来修饰类的构造函数,被修饰的构造函数的类,不能发生相应的隐式类型转换,只能以显示的方式进行类型转换,注意以下几点:

  • explicit 关键字只能用于类内部的构造函数声明上
  • explicit 关键字作用于单个参数的构造函数
  • 被explicit修饰的构造函数的类,不能发生相应的隐式类型转换
posted @ 2021-02-24 12:59  DearLeslie  阅读(345)  评论(0编辑  收藏  举报