C/C++常见知识点以及面试题总结(不定时更新)

整理这篇文章主要是为了自己对C/C++概念的梳理总结。内容来源分布大致为网络、《C++ Primer Plus》和《Effective C++》等。很多大牛已经有很好的总结,参考链接会在文末给出。

一. 计算机基础

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

C中,内存分为5个区:堆(malloc)、栈(如局部变量、函数参数)、程序代码区(存放二进制代码)、全局/静态存储区(全局变量、static变量)和常量存储区(常量)。此外,C++中有自由存储区(new)一说。
全局变量、static变量会初始化为缺省值,而堆和栈上的变量是随机的,不确定的。

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

  • 总的来说,堆是C语言和操作系统的术语,是操作系统维护的一块动态分配内存;自由存储是C++中通过new与delete动态分配和释放对象的抽象概念。他们并不是完全一样。
  • 从技术上来说,堆(heap)是C语言和操作系统的术语。堆是操作系统所维护的一块特殊内存,它提供了动态分配的功能,当运行程序调用malloc()时就会从中分配,稍后调用free可把内存交还。而自由存储是C++中通过new和delete动态分配和释放对象的抽象概念,通过new来申请的内存区域可称为自由存储区。基本上,所有的C++编译器默认使用堆来实现自由存储,也即是缺省的全局运算符new和delete也许会按照malloc和free的方式来被实现,这时藉由new运算符分配的对象,说它在堆上也对,说它在自由存储区上也正确。

3. 堆和栈的区别?

  1. 堆存放动态分配的对象——即那些在程序运行时动态分配的对象,比如 new 出来的对象,其生存期由程序控制;
  2. 栈用来保存定义在函数内的非static对象,如局部变量,仅在其定义的程序块运行时才存在;
  3. 静态内存用来保存static对象,类static数据成员以及定义在任何函数外部的变量,static对象在使用之前分配,程序结束时销毁;
  4. 栈和静态内存的对象由编译器自动创建和销毁。

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。更多可以参考浮点数表示。
  • 无论是单精度还是双精度在存储中都分为三个部分:

1). 符号位(Sign) : 0代表正,1代表为负
2). 指数位(Exponent):用于存储科学计数法中的指数数据,并且采用移位存储
3). 尾数部分(Mantissa):尾数部分

单精度float的存储方式如下:
在这里插入图片描述
双精度double的存储方式如下:
在这里插入图片描述

6. 左值和右值?

  • 不是很严谨的来说,左值指的是既能够出现在等号左边也能出现在等号右边的变量(或表达式),右值指的则是只能出现在等号右边的变量(或表达式)。
int a = 1;     //a是一个左值
int b = 2;     //b是一个左值
int c = a + b; //+需要右值,所以a和b都转换成右值,并且返回一个右值

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

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

1). 使用的时候要记得指针的长度.
2). malloc的时候得确定在那里free;new的时候记得delete。
3). 对指针赋值的时候应该注意被赋值指针需要不需要释放.
4). 动态分配内存的指针最好不要再次赋值.
5). 在C++中应该优先考虑使用智能指针.

二. C与C++

1. C和C++的区别?

  1. C++是C的超集;
  2. C是一个结构化语言,它的重点在于算法和数据结构。C程序的设计首要考虑的是如何通过一个过程,对输入(或环境条件)进行运算处理得到输出(或实现过程(事务)控制),而对于C++,首要考虑的是如何构造一个对象模型,让这个模型能够契合与之对应的问题域,这样就可以通过获取对象的状态信息得到输出或实现过程(事务)控制。

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

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

  • 在c中,int fun() 会解读为返回值为int(即使前面没有int,也是如此,但是在c++中如果没有返回类型将报错),输入类型和个数没有限制, 而int fun(void)则限制输入类型为一个void。
  • 在c++下,这两种情况都会解读为返回int类型,输入void类型。

3. const 有什么用途

  1. 定义只读变量,或者常量(只读变量和常量的区别参考下面一条);
  2. 修饰函数的参数和函数的返回值;
  3. 修饰函数的定义体,这里的函数为类的成员函数,被const修饰的成员函数代表不能修改成员变量的值,因此const成员函数只能调用const成员函数;
  4. 只读对象。只读对象只能调用const成员函数。

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

在c中不能。c中的const仅仅是从编译层来限定,不允许对const 变量进行赋值操作,在运行期是无效的,所以并非是真正的常量(比如通过指针对const变量是可以修改值的),但是c++中是有区别的,c++在编译时会把const常量加入符号表,以后(仍然在编译期)遇到这个变量会从符号表中查找,所以在C++中是不可能修改到const变量的

补充:

  1. c中的局部const常量存储在栈空间,全局const常量存在只读存储区,所以全局const常量也是无法修改的,它是一个只读变量。
  2. 这里需要说明的是,常量并非仅仅是不可修改,而是相对于变量,它的值在编译期已经决定,而不是在运行时决定。
  3. c++中的const 和宏定义是有区别的,宏是在预编译期直接进行文本替换,而const发生在编译期,是可以进行类型检查和作用域检查的。
  4. c语言中只有enum可以实现真正的常量。
  5. c++中只有用字面量初始化的const常量会被加入符号表,而变量初始化的const常量依然只是只读变量。
  6. c++中const成员为只读变量,可以通过指针修改const成员的值,另外const成员变量只能在初始化列表中进行初始化。

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

  1. 首先宏是C中引入的一种预处理功能;
  2. 内联(inline)函数是C++中引用的一个新的关键字;C++中推荐使用内联函数来替代宏代码片段;
  3. 内联函数将函数体直接扩展到调用内联函数的地方,这样减少了参数压栈,跳转,返回等过程;
  4. 由于内联发生在编译阶段,所以内联相较宏,是有参数检查和返回值检查的,因此使用起来更为安全;
  5. 需要注意的是, inline会向编译期提出内联请求,但是是否内联由编译期决定(当然可以通过设置编译器,强制使用内联);
  6. 由于内联是一种优化方式,在某些情况下,即使没有显示的声明内联,比如定义在class内部的方法,编译器也可能将其作为内联函数。
  7. 内联函数不能过于复杂,最初C++限定不能有任何形式的循环,不能有过多的条件判断,不能对函数进行取地址操作等,但是现在的编译器几乎没有什么限制,基本都可以实现内联。

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

  1. malloc与free是C++/C语言的标准库函数,new/delete是C++的运算符。它们都可用于申请动态内存和释放内存。
  2. 对于非内部数据类型的对象而言,光用maloc/free无法满足动态对象的要求。对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数。
    由于malloc/free是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于malloc/free。因此C++语言需要一个能完成动态内存分配和初始化工作的运算符new,以一个能完成清理与释放内存工作的运算符delete。注意new/delete不是库函数。

最后补充一点题外话,new 在申请内存的时候就可以初始化(如下代码), 而malloc是不允许的。另外,由于malloc是库函数,需要相应的库支持,因此某些简易的平台可能不支持,但是new就没有这个问题了,因为new是C++语言所自带的运算符。

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

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

  1. static_cast
    a. 用于基本类型间的转换
    b. 不能用于基本类型指针间的转换
    c. 用于有继承关系类对象间的转换和类指针间的转换
  2. dynamic_cast
    a. 用于有继承关系的类指针间的转换
    b. 用于有交叉关系的类指针间的转换
    c. 具有类型检查的功能
    d. 需要虚函数的支持
  3. reinterpret_cast
    a. 用于指针间的类型转换
    b. 用于整数和指针间的类型转换
  4. const_cast
    a. 用于去掉变量的const属性
    b. 转换的目标类型必须是指针或者引用

在C++中,普通类型可以通过类型转换构造函数转换为类类型,那么类可以转换为普通类型吗?答案是肯定的。但是在工程应用中一般不用类型转换函数,因为无法抑制隐式的调用类型转换函数(类型转换构造函数可以通过explicit来抑制其被隐式的调用),而隐式调用经常是bug的来源。实际工程中替代的方式是定义一个普通函数,通过显式的调用来达到类型转换的目的。

8. static 有什么用途

  1. 静态(局部/全局)变量
  2. 静态函数
  3. 类的静态数据成员
  4. 类的静态成员函数

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

静态成员变量

  1. 静态成员变量需要在类内声明(加static),在类外初始化(不能加static),如下例所示;
  2. 静态成员变量在类外单独分配存储空间,位于全局数据区,因此静态成员变量的生命周期不依赖于类的某个对象,而是所有类的对象共享静态成员变量;
  3. 可以通过对象名直接访问公有静态成员变量;
  4. 可以通过类名直接调用公有静态成员变量,即不需要通过对象,这一点是普通成员变量所不具备的。
class example{
    
    
private:
static int m_int; //static成员变量
};

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

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

静态成员函数

  1. 静态成员函数是类所共享的;
  2. 静态成员函数可以访问静态成员变量,但是不能直接访问普通成员变量(需要通过对象来访问);需要注意的是普通成员函数既可以访问普通成员变量,也可以访问静态成员变量;
  3. 可以通过对象名直接访问公有静态成员函数;
  4. 可以通过类名直接调用公有静态成员函数,即不需要通过对象,这一点是普通成员函数所不具备的。
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 的区别?

相同点:

它们的作用是防止头文件被重复包含。

不同点:

  1. ifndef 由语言本身提供支持,但是 program once 一般由编译器提供支持,也就是说,有可能出现编译器不支持的情况(主要是比较老的编译器)。
  2. 通常运行速度上 ifndef 一般慢于 program once,特别是在大型项目上, 区别会比较明显,所以越来越多的编译器开始支持 program once。
  3. ifndef 作用于某一段被包含(define 和 endif 之间)的代码, 而 program once 则是针对包含该语句的文件, 这也是为什么 program once 速度更快的原因。
  4. 如果用 ifndef 包含某一段宏定义,当这个宏名字出现“撞车”时,可能会出现这个宏在程序中提示宏未定义的情况(在编写大型程序时特性需要注意,因为有很多程序员在同时写代码)。相反由于program once 针对整个文件, 因此它不存在宏名字“撞车”的情况, 但是如果某个头文件被多次拷贝,program once 无法保证不被多次包含,因为program once 是从物理上判断是不是同一个头文件,而不是从内容上。

三. 数组、指针 & 引用

1. 指针和引用的区别?

相同点:

  1. 都是地址的概念;
  2. 都是“指向”一块内存。指针指向一块内存,它的内容是所指内存的地址;而引用则是某块内存的别名;
  3. 引用在内部实现其实是借助指针来实现的,一些场合下引用可以替代指针,比如作为函数形参。

不同点:

  1. 指针是一个实体,而引用(看起来,这点很重要)仅是个别名;
  2. 引用只能在定义时被初始化一次,之后不可变;指针可变;引用“从一而终”,指针可以“见异思迁”;
  3. 引用不能为空,指针可以为空;
  4. “sizeof 引用”得到的是所指向的变量(对象)的大小,而“sizeof 指针”得到的是指针本身的大小;
  5. 指针和引用的自增(++)运算意义不一样;
  6. 引用是类型安全的,而指针不是 (引用比指针多了类型检查)
  7. 引用具有更好的可读性和实用性。

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

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

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

int main(void)
{
    
    
        int a = 8;
        const int &b = a;
        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;
}

四. C++特性

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

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

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

封装:

  • 封装是实现面向对象程序设计的第一步,封装就是将数据或函数等集合在一个个的单元中(我们称之为类)。
  • 封装的意义在于保护或者防止代码(数据)被我们无意中破坏。

继承:

继承主要实现重用代码,节省开发时间。子类可以继承父类的一些东西。

  • a. 公有继承(public)
    公有继承的特点是基类的公有成员和保护成员作为派生类的成员时,它们都保持原有的状态,而基类的私有成员仍然是私有的,不能被这个派生类的子类所访问。
  • b. 私有继承(private)
    私有继承的特点是基类的公有成员和保护成员都作为派生类的私有成员,并且不能被这个派生类的子类所访问。
  • c. 保护继承(protected)
    保护继承的特点是基类的所有公有成员和保护成员都成为派生类的保护成员,并且只能被它的派生类成员函数或友元访问,基类的私有成员仍然是私有的。

多态:

  • 函数的多态又称为函数的重载,函数参数的数目、类型和排列顺序中只要有一项不相同,而且有相同函数名则称之函数的多态。
  • 在类中,多态就是许多情况下可以互换地使用基类型和派生类型的多种形态。依赖于动态绑定机制。
  • C++中默认不使用动态绑定(默认为静态联编,而不是动态联编)。要触发动态绑定必须满足两个条件:
  1. 接口函数的形式参数必须是引用类型或者指针类型
  2. 动态执行函数(对象参数的成员函数而非接口函数)必须是声明为虚成员函数。

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

  1. 没有任何构造函数时,编译器会自动生成默认构造函数,也就是无参构造函数;当类没有拷贝构造函数时,会生成默认拷贝构造函数。
  2. 深拷贝是指拷贝后对象的逻辑状态相同,而浅拷贝是指拷贝后对象的物理状态相同;默认拷贝构造函数属于浅拷贝。
  3. 当系统中有成员指代了系统中的资源时,需要深拷贝。比如指向了动态内存空间,打开了外存中的文件或者使用了系统中的网络接口等。如果不进行深拷贝,比如动态内存空间,可能会出现多次被释放的问题。是否需要定义拷贝构造函数的原则是,是类是否有成员调用了系统资源,如果定义拷贝构造函数,一定是定义深拷贝,否则没有意义。

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

构造函数执行顺序

1). 首先调用父类的构造函数;
2). 调用成员变量的构造函数;
3). 调用类自身的构造函数。

析构函数

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

参考:
https://blog.csdn.net/kuweicai/article/details/82779648#t9

猜你喜欢

转载自blog.csdn.net/QLeelq/article/details/113615314