C++面试问题

  1. C++有两种方式定义常量

  • 用#define预处理器

  • 用const关键字:const int LENGTH = 10;

    注意:定义const的常量接下来再不能赋值

    1
    2
    3
    4
    5
    > const double pi;                   
    > pi=3.14159265;
    > 修改为:
    > const double pi=3.141592;
    >

宏定义 #define 和常量 const 的区别

  • 类型和安全检查不同

宏定义是字符替换,没有数据类型的区别,同时这种替换没有类型安全检查,可能产生边际效应等错误;

const常量是常量的声明,有类型区别,需要在编译阶段进、行类型检查

  • 编译器处理不同

宏定义是一个”编译时”概念,在预处理阶段展开,不能对宏定义进行调试,生命周期结束于编译时期;

const常量是一个”运行时”概念,在程序运行使用,类似于一个只读行数据

  • 存储方式不同

宏定义是直接替换,不会分配内存,存储与程序的代码段中;

const常量需要进行内存分配,存储与程序的数据段中

  • 定义域不同
1
2
3
4
5
6
7
8
9
10
void f1 ()
{
#define N 12
const int n 12;
}
void f2 ()
{
cout<<N <<endl; //正确,N已经定义过,不受定义域限制
cout<<n <<endl; //错误,n定义域只在f1函数中
}
  • 定义后能否取消

宏定义可以通过#undef来使之前的宏定义失效

const常量定义后将在定义域内永久有效

1
2
3
4
5
6
7
8
void f1()
{
#define N 12
const int n = 12;

#undef N //取消宏定义后,即使在f1函数中,N也无效了
#define N 21//取消后可以重新定义
}
  • 是否可以做函数参数

宏定义不能作为参数传递给函数

const常量可以在函数的参数列表中出现

const修饰成员方法

1
void showData()const{ }

const 修饰的成员函数中不能修改成员变量,不能调用非 const 修饰的函数

  1. 关键字explicit

    C++提供了关键字explicit,可以阻止不应该允许的经过转换构造函数进行的隐式转换的发生。声明为explicit的构造函数不能在隐式转换中使用。

    explicit构造函数是用来防止隐式转换的。请看下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Test1
{
public:
Test1(int n)
{
num=n;
}//普通构造函数
private:
int num;
};
class Test2
{
public:
explicit Test2(int n)
{
num=n;
}//explicit(显式)构造函数
private:
int num;
};
int main()
{
Test1 t1=12;//隐式调用其构造函数,成功
Test2 t2=12;//编译错误,不能隐式调用其构造函数
Test2 t2(12);//显式调用成功
return 0;
}

​ Test1的构造函数带一个int型的参数,代码23行会隐式转换成调用Test1的这个构造函数。而Test2的构造函数被声明为explicit(显式),这表示不能通过隐式转换来调用这个构造函数,因此代码24行会出现编译错误。

​ 普通构造函数能够被隐式调用。而explicit构造函数只能被显式调用。

  1. 关键字volatile

    往往会用于多线程的修饰,比如:

1
2
3
4
5
6
7
8
9
10
11
12
volatile boolean isNext = false;

Thread A() {
// 第一个工作
// isNext = true;
}

Thread B (){
if (isNext) {
// 第二个工作
}
}

​ 这里volatile 就是从来标记isNext, 以确保线程B每次都重新从内存中读取isNext的值,第二个工作一定在第一个工作之后进行。

​ 但是要注意,这里无法保证顺序性,应该编译器编译的时候会重新打乱两个语句的先后顺序,因此做第一个工作和赋值给isNext不一定会按照你代码顺序正常执行。

  1. c++存储类

    存储类定义 C++ 程序中变量/函数的范围(可见性)和生命周期。这些说明符放置在它们所修饰的类型之前。下面列出 C++ 程序中可用的存储类:

  • auto
  • register
  • static
  • extern
  • mutable
  • thread_local (C++11)

从 C++ 11 开始,auto 关键字不再是 C++ 存储类说明符,且 register 关键字被弃用。

  • auto 存储类

    自 C++ 11 以来,**auto** 关键字用于两种情况:
    

1,声明变量时根据初始化表达式自动推断该变量的类型

2,声明函数时函数返回值的占位符。

​ C++98标准中auto关键字用于自动变量的声明,但由于使用极少且多余,在C++11中已删除这一用法。

根据初始化表达式自动推断被声明的变量的类型,如:

1
2
3
4
auto f=3.14;      //double
auto s("hello"); //const char
auto z = new auto(9); // int*
auto x1 = 5, x2 = 5.0, x3='r';//错误,必须是初始化为同一类型

  • register 存储类

    **register** 存储类用于定义存储在寄存器中而不是 RAM 中的局部变量。
    
    寄存器只用于需要快速访问的变量,比如计数器。还应注意的是,定义 'register' 并不意味着变量将被存储在寄存器中,它意味着变量可能存储在寄存器中,这取决于硬件和实现的限制。
    

  • static 存储类

    1,使用 static 修饰局部变量可以在函数调用之间保持局部变量的值。
    
    2,用在全局变量,使变量的作用域限制在声明它的文件内。
    
    3, 用在类数据成员上时,会导致仅有一个该成员的副本被类的所有对象共享。
    

下面展示static局部变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>

// 函数声明
void func(void);

static int count = 10; /* 全局变量 */

int main()
{
while(count--)
{
func();
}
return 0;
}
// 函数定义
void func( void )
{
static int i = 5; // 局部静态变量
i++;
std::cout << "变量 i 为 " << i ;
std::cout << " , 变量 count 为 " << count << std::endl;
}

当上面的代码被编译和执行时,它会产生下列结果:

1
2
3
4
5
6
7
8
9
10
变量 i 为 6 , 变量 count 为 9
变量 i 为 7 , 变量 count 为 8
变量 i 为 8 , 变量 count 为 7
变量 i 为 9 , 变量 count 为 6
变量 i 为 10 , 变量 count 为 5
变量 i 为 11 , 变量 count 为 4
变量 i 为 12 , 变量 count 为 3
变量 i 为 13 , 变量 count 为 2
变量 i 为 14 , 变量 count 为 1
变量 i 为 15 , 变量 count 为 0

  • extern 存储类

    **extern** 存储类用于提供一个全局变量的引用,全局变量对所有的程序文件都是可见的。
    
    当您有多个文件且定义了一个可以在其他文件中使用的全局变量或函数时,可以在其他文件中使用 *extern* 来得到已定义的变量或函数的引用。可以这么理解,*extern* 是用来在另一个文件中声明一个全局变量或函数。
    
    extern 修饰符通常用于当有两个或多个文件共享相同的全局变量或函数的时候,如下所示:
    

第一个文件:main.cpp

1
2
3
4
5
6
7
8
9
10
#include <iostream>

int count ;
extern void write_extern();

int main()
{
count = 5;
write_extern();
}

第二个文件:support.cpp

1
2
3
4
5
6
7
8
#include <iostream>

extern int count;

void write_extern(void)
{
std::cout << "Count is " << count << std::endl;
}

​ 在这里,第二个文件中的 extern 关键字用于声明已经在第一个文件 main.cpp 中定义的 count。现在 ,编译这两个文件,如下所示:

1
$ g++ main.cpp support.cpp -o write

这会产生 write 可执行程序,尝试执行 write,它会产生下列结果:

1
2
$ ./write
Count is 5
  • mutable 存储类

    **mutable** 说明符仅适用于类的对象,这将在本教程的最后进行讲解。它允许对象的成员替代常量。也就是说,mutable 成员可以通过 const 成员函数修改。
    
  • thread_local 存储类

    使用 thread_local 说明符声明的变量仅可在它在其上创建的线程上访问。 变量在创建线程时创建,并在销毁线程时销毁。 每个线程都有其自己的变量副本。thread_local 说明符可以与 static 或 extern 合并。
    
    可以将 thread_local 仅应用于数据声明和定义,thread_local 不能用于函数声明或定义。
    

以下演示了可以被声明为 thread_local 的变量:

1
2
3
4
5
6
7
8
9
10
11
thread_local int x;  // 命名空间下的全局变量
class X
{
static thread_local std::string s; // 类的static成员变量
};
static thread_local std::string X::s; // X::s 是需要定义的

void foo()
{
thread_local std::vector<int> v; // 本地变量
}
  1. 引用 vs 指针

引用很容易与指针混淆,它们之间有三个主要的不同:

  • 不存在空引用。引用必须连接到一块合法的内存。
  • 一旦引用被初始化为一个对象,就不能被指向到另一个对象。指针可以在任何时候指向到另一个对象。
  • 引用必须在创建时被初始化。指针可以在任何时间被初始化。
  1. c++五大存储区

    在C++中,内存分成5个区,他们分别是堆、栈、自由存储区、全局/静态存储区和常量存储区。

    • 栈,就是那些由编译器在需要的时候分配,在不需要的时候自动清楚的变量的存储区。里面的变量通常是局部变量、函数参数等。
    • 堆,就是那些由new分配的内存块,他们的释放编译器不去管,由我们的应用程序去控制,一般一个new就要对应一个delete。如果程序员没有释放掉,那么在程序结束后,操作系统会自动回收。
    • 自由存储区,就是那些由malloc等分配的内存块,他和堆是十分相似的,不过它是用free来结束自己的生命的。
    • 全局/静态存储区,全局变量和静态变量被分配到同一块内存中,在以前的C语言中,全局变量又分为初始化的和未初始化的,在C++里面没有这个区分了,他们共同占用同一块内存区。
    • 常量存储区,这是一块比较特殊的存储区,他们里面存放的是常量,不允许修改
  1. 析构函数为虚函数

1
2
3
4
5
Derived *p=new Derived; //先释放派生类,再释放基类
Base *p=new Derived; //只删除基类

virtual ~Base(){};
Base *p=new Derived //先删除派生类,再释放基类

​ 纯虚函数声明如下: virtual void funtion1()=0; 纯虚函数一定没有定义,纯虚函数用来规范派生类的行为,即接口。包含纯虚函数的类是抽象类,抽象类不能定义实例,但可以声明指向实现该抽象类的具体类的指针或引用。

注意:虚继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
using namespace std;
class Person{
public: Person(){ cout<<"Person构造"<<ENDL; }
~Person(){ cout<<"Person析构"<<ENDL; }
};
class Teacher : virtual public Person{
public: Teacher(){ cout<<"Teacher构造"<<ENDL; }
~Teacher(){ out<<"Teacher析构"<<ENDL; }
};
class Student : virtual public Person{
public: Student(){ cout<<"Student构造"<<ENDL; }
~Student(){ cout<<"Student析构"<<ENDL; }
};
class TS : public Teacher, public Student{
public: TS(){ cout<<"TS构造"<<ENDL; }
~TS(){ cout<<"TS析构"<<ENDL; }
};
int main(int argc,char* argv[])
{
TS ts;
return 0;
}

这段代码的终端输出结果为:
Person构造
Teacher构造
Student构造
TS构造
TS析构
Student析构
Teacher析构
Person析构

当Teacher类和Student类没有虚继承Person类的时候,也就是把virtual去掉时候终端输出的结果为:
Person构造
Teacher构造
Person构造
Student构造
TS构造
TS析构
Student析构
Person析构
Teacher析构
Person析构

​ 大家可以很清楚的看到这个结果明显不是我们所期望的。我们在构造TS的时候需要先构造他的基类,也就是Teacher类和Student类。而Teacher类和Student类由都继承于Person类。这样就导致了构造TS的时候实例化了两个Person类。同样的道理,析构的时候也是析构了两次Person类,这是非常危险的,也就引发出了virtual的第三种用法,虚析构。

  1. 注意数据抽象,数据封装,数据接口的表达

    • 数据抽象:仅向用户暴露接口而把具体的实现细节隐藏起来的机制。

    • 数据封装:把数据和操作数据的函数捆绑在一起的机制

    • 数据接口:为了给其他类提供一个可以继承的适当的基类,射界抽象类且不能被用于实例化对象,它只能作为接口使用。

  2. C++ 中的预定义宏

    C++ 提供了下表所示的一些预定义宏:

    | 宏 | 描述 |
    | ——- | ———————————————————— |
    | _LINE__ | 这会在程序编译时包含当前行号。 |
    | _FILE__ | 这会在程序编译时包含当前文件名。 |
    | _DATE__ | 这会包含一个形式为 month/day/year 的字符串,它表示把源文件转换为目标代码的日期。 |
    | _TIME__ | 这会包含一个形式为 hour:minute:second 的字符串,它表示程序被编译的时间。 |