C/C++ 命中率比较高的面试知识点,你都答得上来吗

564次阅读  |  发布于2年以前

以下内容部分整理自网络,部分为自己面试的真题。

第一部分:计算机基础

1 . C/C++内存有哪几种类型?

C中,内存分为5个区:堆(malloc)、栈(如局部变量、函数参数)、程序代码区(存放二进制代码)、全局/静态存储区(全局变量、static变量)和常量存储区(常量)。此外,C++中有自由存储区(new)一说。

全局变量、static变量会初始化为缺省值,而堆和栈上的变量是随机的,不确定的。

2 . 堆和栈的区别?

3 . 堆和自由存储区的区别?

总的来说,堆是C语言和操作系统的术语,是操作系统维护的一块动态分配内存;自由存储是C++中通过new与delete动态分配和释放对象的抽象概念。他们并不是完全一样。

从技术上来说,堆(heap)是C语言和操作系统的术语。堆是操作系统所维护的一块特殊内存,它提供了动态分配的功能,当运行程序调用malloc()时就会从中分配,稍后调用free可把内存交还。

而自由存储是C++中通过new和delete动态分配和释放对象的抽象概念,通过new来申请的内存区域可称为自由存储区。

基本上,所有的C++编译器默认使用堆来实现自由存储,也即是缺省的全局运算符new和delete也许会按照malloc和free的方式来被实现,这时藉由new运算符分配的对象,说它在堆上也对,说它在自由存储区上也正确。

4 . 程序编译的过程?

程序编译的过程中就是将用户的文本形式的源代码(c/c++)转化成计算机可以直接执行的机器代码的过程。主要经过四个过程:预处理、编译、汇编和链接。具体示例如下。

一个hello.c的c语言程序如下。

#include <stdio.h>
int main()
{
    printf("happy new year!\n");
    return 0;
}

其编译过程如下:

在这里插入图片描述

5 . 计算机内部如何存储负数和浮点数?

负数比较容易,就是通过一个标志位和补码来表示。 拓展问题:

对于浮点类型的数据采用单精度类型(float)和双精度类型(double)来存储,float数据占用32bit,double数据占用64bit,我们在声明一个变量float f= 2.25f的时候,是如何分配内存的呢?

如果胡乱分配,那世界岂不是乱套了么,其实不论是float还是double在存储方式上都是遵从IEEE的规范的,float遵从的是IEEE R32.24 ,而double 遵从的是R64.53。更多可以参考浮点数表示。

无论是单精度还是双精度在存储中都分为三个部分:

6 . 函数调用的过程?

如下结构的代码,

int main(void)
{
  ...
  d = fun(a, b, c);
  cout<<d<<endl;
  ...
  return 0;
}

调用fun()的过程大致如下:

7 . 左值和右值

不是很严谨的来说,左值指的是既能够出现在等号左边也能出现在等号右边的变量(或表达式),右值指的则是只能出现在等号右边的变量(或表达式)。举例来说我们定义的变量 a 就是一个左值,而malloc返回的就是一个右值。

或者左值就是在程序中能够寻址的东西,右值就是一个具体的真实的值或者对象,没法取到它的地址的东西(不完全准确),因此没法对右值进行赋值,但是右值并非是不可修改的,比如自己定义的class, 可以通过它的成员函数来修改右值。

归纳一下就是:

8 . 什么是内存泄漏?面对内存泄漏和指针越界,你有哪些方法?你通常采用哪些方法来避免和减少这类错误?

用动态存储分配函数动态开辟的空间,在使用完毕后未释放,结果导致一直占据该内存单元即为内存泄露。

第二部分:C v.s. C++

1 . C和C++的区别?

2 . int fun() 和 int fun(void)的区别?

这里考察的是c 中的默认类型机制。

3 . const 有什么用途

主要有三点:

class Screen {
public:
const char cha; //const成员变量
char get() const; //const成员函数
};

const Screen screen; //只读对象

4 . 在C中用const 能定义真正意义上的常量吗?C++中的const呢?

不能。c中的const仅仅是从编译层来限定,不允许对const 变量进行赋值操作,在运行期是无效的,所以并非是真正的常量(比如通过指针对const变量是可以修改值的)。

但是c++中是有区别的,c++在编译时会把const常量加入符号表,以后(仍然在编译期)遇到这个变量会从符号表中查找,所以在C++中是不可能修改到const变量的。 补充:

下面我们通过代码来看看区别。 同样一段代码,在c编译器下,打印结果为*pa = 4,a = 4 在c++编译下打印的结果为 *pa = 4, a = 8

int main(void)
{
    const int a = 8;
    int *pa = (int *)&a;
    *pa = 4;
    printf("*pa = %d, a = %d", *pa, a);
    return 0;
}

另外值得一说的是,由于c++中const常量的值在编译期就已经决定,下面的做法是OK的,但是c中是编译通不过的。

int main(void)
{
    const int a = 8;
    const int b = 2;
    int array[a+b] = {0};
    return 0;
}

5 . 宏和内联(inline)函数的比较?

6 . C++中有了malloc / free , 为什么还需要 new / delete?

int *p = new int(1);

特别的,在C++中,如下的代码,用new创建一个对象(new 会触发构造函数, delete会触发析构函数),但是malloc仅仅申请了一个空间,所以在C++中引入new和delete来支持面向对象。

#include <cstdlib>
class Test
{
    ...
}

Test* pn = new Test;
Test* pm = (Test*)malloc(sizeof(Test));

7 . C和C++中的强制类型转换?

C中是直接在变量或者表达式前面加上(小括号括起来的)目标类型来进行转换,一招走天下,操作简单,但是由于太过直接,缺少检查,因此容易发生编译检查不到错误,而人工检查又及其难以发现的情况;而C++中引入了下面四种转换:

拓展 在C++中,普通类型可以通过类型转换构造函数转换为类类型,那么类可以转换为普通类型吗?答案是肯定的。

但是在工程应用中一般不用类型转换函数,因为无法抑制隐式的调用类型转换函数(类型转换构造函数可以通过explicit来抑制其被隐式的调用),而隐式调用经常是bug的来源。

实际工程中替代的方式是定义一个普通函数,通过显式的调用来达到类型转换的目的。

class test{
    int m_value;
    ...
public:
    operator int()  //类型转换函数
    {
        return m_value;
    }

    int toInt() //显示调用普通函数来实现类型转换
    {
        return m_value
    }
};

int main()
{
    ...
    test a(5);
    int i = a;    // 相当于 int i = test::operator int(&a)
    ...

    return 0;
}

8 . static 有什么用途

9 . 类的静态成员变量和静态成员函数各有哪些特性?

静态成员变量

class example{
public:
static int m_int; //static成员变量
};

int example::m_int = 0; //没有static

cout<<example::m_int; //可以直接通过类名调用静态成员变量

静态成员函数

class example{
private:
static int m_int_s; //static成员变量
int m_int;
static int getI() //静态成员函数在普通成员函数前加static即可
{
  return m_int_s; //如果返回m_int则报错,但是可以return d.m_int是合法的
}
};

cout<<example::getI(); //可以直接通过类名调用静态成员变量

10 . 在C++程序中调用被C编译器编译后的函数,为什么要加extern“C”?

C++语言支持函数重载,C语言不支持函数重载,函数被C++编译器编译后在库中的名字与C语言的不同,假设某个函数原型为:

          void foo(int x, int y);

该函数被C编译器编译后在库中的名字为 _foo, 而C++编译器则会产生像: _foo_int_int 之类的名字。为了解决此类名字匹配的问题,C++提供了C链接交换指定符号 extern “C”。

11 . 头文件中的 ifndef/define/endif 是干什么用的? 该用法和 program once 的区别?

相同点: 它们的作用是防止头文件被重复包含。 不同点

12 . 当i是一个整数的时候++i和i++那个更快一点?i++和++i的区别是什么?

答:理论上++i更快,实际与编译器优化有关,通常几乎无差别。

//i++实现代码为:
int operator++(int)
{
    int temp = *this;
    ++*this;
    return temp;
}//返回一个int型的对象本身

// ++i实现代码为:
int& operator++()
{
    *this += 1;
    return *this;
}//返回一个int型的对象引用

i++和++i的考点比较多,简单来说,就是i++返回的是i的值,而++i返回的是i+1的值。也就是++i是一个确定的值,是一个可修改的左值,如下使用:

cout << ++(++(++i)) << endl;
cout << ++ ++i << endl;

可以不停的嵌套++i。 这里有很多的经典笔试题,一起来观摩下:

int main()
{
    int i = 1;
    printf("%d,%d\n", ++i, ++i);    //3,3
    printf("%d,%d\n", ++i, i++);    //5,3
    printf("%d,%d\n", i++, i++);    //6,5
    printf("%d,%d\n", i++, ++i);    //8,9
    system("pause");
    return 0;
}

首先是函数的参数入栈顺序从右向左入栈的,计算顺序也是从右往左计算的,不过都是计算完以后再进行的压栈操作:

上面的分析也是基于VS搞的,不过准确来说函数多个参数的计算顺序是未定义的(the order of evaluation of function arguments are undefined)。笔试题目的运行结果随不同的编译器而异。

这里还有一个 i++ 的典型应用案例。

    map<char, int> b = {{'a', 1}, {'b', 2}};

    for(auto iter = b.begin(); iter != b.end();){
        if(iter->first == 'a'){
            b.erase(iter++);    // 等价于 auto t = iter; iter = iter + 1; b.erase(t);
        }
        else{
            iter++;
        }
    }

第三部分:数组、指针 & 引用

1 . 指针和引用的区别?

相同点:

不同点:

2 . 引用占用内存空间吗?

如下代码中对引用取地址,其实是取的引用所对应的内存空间的地址。这个现象让人觉得引用好像并非一个实体。但是引用是占用内存空间的,而且其占用的内存和指针一样,因为引用的内部实现就是通过指针来完成的。

比如 Type& name;<===> Type* const name。

int main(void)
{
        int a = 8;
        int &b = a;
        int *p = &b;        // 等价于 int *p = &a;
        *p = 0;
        cout<<a; //output 0
           return 0;
}

3 . 三目运算符

在C中三目运算符(? :)的结果仅仅可以作为右值,比如如下的做法在C编译器下是会报错的,但是C++中却是可以是通过的。这个进步就是通过引用来实现的,因为下面的三目运算符的返回结果是一个引用,然后对引用进行赋值是允许的。

int main(void)
{
        int a = 8;
        int b = 6;
        (a>b ? a : b) = 88;
        cout<<a; //output 88
    return 0;
}

4 . 指针数组和数组指针的区别

数组指针,是指向数组的指针,而指针数组则是指该数组的元素均为指针。

类型名 (*数组标识符)[数组长度]
类型名 *数组标识符[数组长度]

5 . 左值引用与右值引用

该部分主要摘自:c++ 学习笔记

左值引用就是我们通常所说的引用,如下所示。左值引用通常可以看作是变量的别名。

type-id & cast-expression 

// demo
int a = 10
int &b = a

int &c = 10    // 错误,无法对一个立即数做引用

const int &d = 10    // 正确, 常引用引用常数量是ok的,其等价于 const int temp = 10; const int &d = temp   

右值引用是 C++11 新增的特性,其形式如下所示。右值引用用来绑定到右值,绑定到右值以后本来会被销毁的右值的生存期会延长至与绑定到它的右值引用的生存期。

type-id && cast-expression  

// demo
int &&var = 10;    // ok

int a = 10
int &&b = a    // 错误, a 为左值

int &&c = var    // 错误,var 为左值

int &&d = move(a)    // ok, 通过move得到左值的右值引用

在汇编层面右值引用做的事情和常引用是相同的,即产生临时量来存储常量。但是,唯一 一点的区别是,右值引用可以进行读写操作,而常引用只能进行读操作。

6 . 右值引用的意义

 class Stack
{
public:
    // 构造
       Stack(int size = 1000) 
        :msize(size), mtop(0)      
    {
            cout << "Stack(int)" << endl;
           mpstack = new int[size];
    }      
  // 析构
~Stack()
{
        cout &lt;&lt; "~Stack()" &lt;&lt; endl;
        delete[]mpstack;
        mpstack = nullptr;
}

// 拷贝构造
Stack(const Stack &amp;src)
    :msize(src.msize), mtop(src.mtop)
{
        cout &lt;&lt; "Stack(const Stack&amp;)" &lt;&lt; endl;
        mpstack = new int[src.msize];
        for (int i = 0; i &lt; mtop; ++i) {
             mpstack[i] = src.mpstack[i];
        }
}

// 赋值重载
Stack&amp; operator=(const Stack &amp;src)
{
        cout &lt;&lt; "operator=" &lt;&lt; endl;
        if (this == &amp;src)
             return *this;

        delete[]mpstack;

        msize = src.msize;
        mtop = src.mtop;
        mpstack = new int[src.msize];
        for (int i = 0; i &lt; mtop; ++i) {
             mpstack[i] = src.mpstack[i];
        }
        return *this;
}

int getSize() 
{
        return msize;
}

运行结果如下。

  Stack(int)             // 构造s
Stack(int)             // 构造tmp
Stack(const Stack&)    // tmp拷贝构造main函数栈帧上的临时对象
~Stack()               // tmp析构
operator=              // 临时对象赋值给s
~Stack()               // 临时对象析构
~Stack()               // s析构

执行代码的过程中调用拷贝构造,将内存中的内容逐个拷贝,在 C++ 11 中可以借助右值引用实现移动拷贝构造和移动赋值来解决这个问题。

  Stack(Stack &&src)
    :msize(src.msize), mtop(src.mtop)
{
    cout << "Stack(Stack&&)" << endl;
  /*此处没有重新开辟内存拷贝数据,把src的资源直接给当前对象,再把src置空*/
mpstack = src.mpstack;  
src.mpstack = nullptr;

执行结果如下。可以看到,在有拷贝构造和移动拷贝构造函数的时候,优先调用了移动拷贝构造和移动赋值。在移动拷贝构造和移动赋值中直接把资源所有权进行了转移,而非拷贝,这就大大提高了执行效率。

 Stack(int)             // 构造s
Stack(int)             // 构造tmp
Stack(Stack&&)         // 调用带右值引用的拷贝构造函数,直接将tmp的资源给临时对象
~Stack()               // tmp析构
operator=(Stack&&)     // 调用带右值引用的赋值运算符重载函数,直接将临时对象资源给s
~Stack()               // 临时对象析构
~Stack()               // s析构
  struct W  
{  
   W(int&, int&) {}  
};  

struct X  
{  
   X(const int&, int&) {}  
};  

struct Y  
{  
   Y(int&, const int&) {}  
};  

struct Z  
{  
   Z(const int&, const int&) {}  
};


template <typename T, typename A1, typename A2>  
T* factory(A1& a1, A2& a2)  
{  
   return new T(a1, a2);  
} 


template <typename T, typename A1, typename A2>  
T* factory_new(A1&& a1, A2&& a2)  
{  
   return new T(std::forward<A1>(a1), std::forward<A2>(a2));  
}  

// demo
int a = 2;
int b = 2;

W* c = factory<w>(a, b);    // ok
Z* d = factory<Z>(2, 2);    // 错误,2 是右值

W* pw = factory_new<W>(a, b);    // ok
X* px = factory_new<X>(2, b);    // ok
Y* py = factory_new<Y>(a, 2);    // ok
Z* e = factory_new<Z>(2, 2);    // ok
W* f = factory_new<W>(2, 2);    // 错误, 

更多相关内容可以参考:c++——左值、右值、左值引用、右值引用

第四部分:C++特性

1 . 什么是面向对象(OOP)?面向对象的意义?

Object Oriented Programming, 面向对象是一种对现实世界理解和抽象的方法、思想,通过将需求要素转化为对象进行问题处理的一种思想。其核心思想是数据抽象、继承和动态绑定(多态)。 面向对象的意义在于:将日常生活中习惯的思维方式引入程序设计中;将需求中的概念直观的映射到解决方案中;以模块为中心构建可复用的软件系统;提高软件产品的可维护性和可扩展性。

2 . 解释下封装、继承和多态?

class Base  
{  
public:  
    Base(){};  
    virtual ~Base(){};  
protected:  
    int int_pro;  
};
class A : public Base  
{  
public:  
    A(){};  
    A(int da){int_pro = da;}  
    // 通过 obj 对象直接访问 protected 成员
    void Set(A &obj){obj.int_pro = 24;}    
    void PrintPro(){cout << "The proteted data is " << int_pro <<endl;}  
};  

这里特别提一下虚继承。虚继承是解决C++多重继承问题(其一,浪费存储空间;第二,存在二义性问题)的一种手段。比如菱形继承,典型的应用就是 iostream, 其继承于 istream 和 ostream,而 istream 和 ostream 又继承于 ios。

3).多态: 多态是指通过基类的指针或者引用,在运行时动态调用实际绑定对象函数的行为。与之相对应的编译时绑定函数称为静态绑定。多态是设计模式的基础,多态是框架的基础。

3 . 什么时候生成默认构造函数(无参构造函数)?什么时候生成默认拷贝构造函数?什么是深拷贝?什么是浅拷贝?默认拷贝构造函数是哪种拷贝?什么时候用深拷贝?

更多可以参考下面的代码,比较容易混淆的是赋值操作符,其实区分很简单,在出现等号的时候,如果有构造新的对象时调用的就是构造,不然就是调用赋值操作符。特别注意下面的 b 和 f,一个是拷贝构造,一个是构造。

class A {
public:
    A() {
        m = new int[4]{ 1,2,3,4 };
        std::cout << "constructor" << std::endl;
    }
    ~A() {
        if (m != nullptr) {
            delete[] m;
        }
    }
    A(const A& a) {
        this->m = new int[4];
        memcpy(a.m, this->m, this->len * sizeof(int));
        std::cout << "copy constructor" << std::endl;
    }
    // 移动构造
    A(A&& a) : m(a.m) {
        a.m = nullptr; 
        std::cout << "move constructor" << std::endl;
    }
    // 赋值操作符重载
    A& operator= (const A& a) {
        memcpy(a.m, this->m, this->len * sizeof(int));
        std::cout << "operator" << std::endl;
        return *this;
    }

private:
    int len = 4;
    int* m = nullptr;
};

A getA(A a) {
    return a;
}

int main(void)
{
    A a;    // construct

    A b = a;    // copy construct
    A c(a); // copy construct

    A d;    // construct
    d = a;  // operate

    A e = getA(a);  // construct, move construct

    A f = A();  // construct
    return 0;
}

4 . 构造函数和析构函数的执行顺序?

构造函数

析构函数

对于栈对象或者全局对象,调用顺序与构造函数的调用顺序刚好相反,也即后构造的先析构。对于堆对象,析构顺序与delete的顺序相关。

5 . 虚析构函数的作用?

基类采用虚析构函数可以防止内存泄漏。比如下面的代码中,如果基类 A 中不是虚析构函数,则 B 的析构函数不会被调用,因此会造成内存泄漏。

class A{
public:
  A(){}
  //~A(){}
  virtual ~A(){cout << "A disconstruct" << endl;}  // 虚析构
//   ~A(){cout << "A disconstruct" << endl;}  // 析构

};

class B : public A{
public:
  B(){
    // new memory
    // ...
    cout << "B construct" << endl;
  }
  ~B(){
    // delete memory
    // ...
    cout << "B disconstruct" << endl;
  }
};

int main(int argc, char **argv)
{
  A *p = new B;

  // some operations
  // ...

  delete p;  // 由于基类中是虚析构,这里会先调用B的析构函数,然后调用A的析构函数

  return 0;
}

但并不是要把所有类的析构函数都写成虚函数。因为当类里面有虚函数的时候,编译器会给类添加一个虚函数表,里面来存放虚函数指针,这样就会增加类的存储空间。所以,只有当一个类被用来作为基类的时候,才把析构函数写成虚函数。

6 . 细看拷贝构造函数

对于 class A,它的拷贝构造函数如下:

 A::A(const A &a){}

1) 为什么必须是当前类的引用呢?

循环调用。如果拷贝构造函数的参数不是当前类的引用,而是当前类的对象,那么在调用拷贝构造函数时,会将另外一个对象直接传递给形参,这本身就是一次拷贝,会再次调用拷贝构造函数,然后又将一个对象直接传递给了形参,将继续调用拷贝构造函数……这个过程会一直持续下去,没有尽头,陷入死循环。

只有当参数是当前类的引用时,才不会导致再次调用拷贝构造函数,这不仅是逻辑上的要求,也是 C++ 语法的要求。

2) 为什么是 const 引用呢?

拷贝构造函数的目的是用其它对象的数据来初始化当前对象,并没有期望更改其它对象的数据,添加 const 限制后,这个含义更加明确了。

另外一个原因是,添加 const 限制后,可以将 const 对象和非 const 对象传递给形参了,因为非 const 类型可以转换为 const 类型。如果没有 const 限制,就不能将 const 对象传递给形参,因为 const 类型不能直接转换为非 const 类型,这就意味着,不能使用 const 对象来初始化当前对象了。

7 . C++的编译环境

如下图所示,C++的编译环境由如下几部分构成:C++标准库、C语言兼容库、编译器扩展库及编译模块。

在这里插入图片描述

#include<iostream>  //C++标准库,不带".h"
#include<string.h>  //C语言兼容库,由编译器厂商提供

值得注意的是,C语言兼容库功能上跟C++标准库中的C语言子库相同,它的存中主要为了兼容C语言编译器,也就是说如果一个文件只包含C语言兼容库(不包含C++标准库),那么它在C语言编译器中依然可以编译通过。

8 . Most vexing parse

直接上代码吧。下面 f 和 g 是有问题的,这种情况就称为 Most vexing parse。

class A {
public:

    A() { cout << "const without param" << endl; }
    A(int a) { cout << "const with param" << endl; }
    A(const A& b) { cout << "copy construct" << endl; }
};


int main(void)
{

    A a;    // const(construct) without param
    A b(10);    // const with param
    A c = A();  // const without param
    A d = A(10);    // const with param
    A e(d); // copy construct
    A f();  
    A g(A());
    A h{};  // const without param
    A i{A{}};   // const without param

    return 0;
}

问题在哪?

A f();    // 这个是不是可以看做声明了一个返回值为A的函数,函数名为 f,参数无    
A g(A());    // 这个是不是可以看做声明了一个返回值为A的函数,函数名为 g, 参数类型为函数指针,这个函数指针的返回值类型为A,参数无

解决办法参考上面的 h, j。

————————————————

Copyright© 2013-2020

All Rights Reserved 京ICP备2023019179号-8