C\C++面试知识点总结(超全)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/CSDN_dzh/article/details/83856999


关键字const

主要作用:

  1. 修饰变量或对象,说明该变量或对象不能被改变
  2. 修饰指针,分为指向常量的指针(const在星号左边,不能用指针改值,属于底层const)和常指针(const在*号右边,能用指针改值,属于顶层const
  3. 修饰引用,常用于形参,既避免了拷贝,又避免了函数对值的修改
  4. 修饰成员函数,说明该成员函数内不能修改成员变量。此时可以用于区分重载(常对象调用常成员函数)
  5. 修饰成员变量,此时只能在构造函数初始化参数列表初始化

拓展:

与define的区别:

  1. const常量有数据类型,而宏常量没有
  2. 编译器可以对const进行安全检查,而只对宏常量进行字符替换
  3. 有些调试工具只可以对const进行调试

如何修改常成员函数中类的成员变量:

  • 在类的成员变量中,用mutable修饰成员变量即可

关键字static

主要作用:

  1. 修饰普通变量,使变量存储在静态区。在main函数前就分配了空间。有初始值就初始化,没有就默认初始化
  2. 修饰普通函数,防止多人开发时函数名相同
  3. 修饰成员变量,使所有该类的对象共享这一份复制,在实现文件中初始化。不需要生成对象就能访问该成员。class::m_val
  4. 修饰成员函数,此时static成员函数没有this指针,故只能访问static成员变量。不需要生成对象就能使用该函数。class::m_func
  5. 函数体内的static变量作用范围仅为该函数体,不同于auto变量,该变量的内存只被分配一次。在下一次调用该函数时依然维持上一次的值

this指针

  1. 是一个隐含于每一个非静态成员函数中的指针(全局函数和静态函数没有),指向正被该成员函数操作的那个对象
  2. 本质是成员函数的第一个参数T*const this(常指针)。常成员函数第一个参数为const T *const this。调用成员函数时,编译器将类的指针作为实参传递进去,成员函数内隐含使用this指针访问数据成员
  3. this不是一个常规变量,只是一个右值,不能取其地址&this
  4. this指针并不占用对象的空间,跟对象之间没有包含关系,只是当前调用的对象被它指向而已
  5. 避免自赋值if (this == &rhs) return *this;

inline内联函数

inline必须与函数定义放在一起才能使函数成为内联,仅放在函数声明前不起作用

例:
普通函数:声明定义都以inline修饰
成员函数:类内定义则隐式当成内联函数;类外定义则类外定义处使用inline修饰

优点:

  1. 内联函数同宏函数一样在被调用处进行代码展开省去了函数调用的开销(参数压栈、栈帧开辟和回收、结果返回等),从而提高程序的运行速度
  2. 内联函数相较于宏,在代码展开时会做安全检查或自动类型转换,且可以在运行时调试,真正具有函数特性
  3. 类的声明中定义的函数除了虚函数的其他函数都隐式地当成内联函数

缺点:

  1. 代码膨胀(复制):内联是通过代码复制来消除函数调用的开销,每一处内联函数调用都要复制代码,使总代码量增大
  2. 不能包含循环、递归、switch等复杂操作
  3. 是否对函数内联,决定权在编译器

扩展:

虚函数能否是内联函数?

  • 可以是,若虚函数是通过对象来访问的,在编译期间就能确定,于是可以内联,但最终是否内联取决于编译器
  • 若虚函数是通过基类指针来访问的,呈现多态性,在编译期间不能确定,编译器无法知道运行期调用哪个代码,于是就不能内联。否则在delete基类指针时会先调用派生类的析构函数,再调用基类析构函数

assert断言

  1. 是一个宏,并非函数
  2. 原型定义在 <assert.h>(C)<cassert>(C++)
  3. 如果它的条件返回false,则输出错误信息,程序终止执行

sizeof关键字(操作符)和字节对齐

sizeof介绍

并非函数返回字节数
括号内的内容在编译过程中是不被编译的,而是被类型替代

字节对齐

字节对齐是在编译时决定的,一旦决定则不会再改变。

  1. 作用于数据类型
    在这里插入图片描述
  2. 作用于数组,则得到数组所占空间的大小
  3. 作用于指针,则得到指针本身所占空间的大小(一般为4)
  4. 作用于变量,则得到变量类型所占空间的大小
    在这里插入图片描述
  5. 作用于普通结构体和类,结构体长度一定是最长的数据元素的整数倍结构体内类型相同的连续元素和数组一样,将在连续空间内
    在这里插入图片描述
    在这里插入图片描述
  6. 作用于带有虚函数或者虚继承的类
    在这里插入图片描述
  7. pack预处理指令:禁止对齐调整(不要轻易做这样的调整,会降低程序性能),合法参数是1\2\4\8\16
    在这里插入图片描述
    在这里插入图片描述

总结:

  • 当作用于结构类型或变量,sizeof返回实际的大小
  • 当作用于静态的数组,sizeof返回全部数组的大小
  • sizeof不能返回被动态分配的数组或外部的数组大小
  • 数组作为参数传递给函数时传递的是指针而不是数组,于是函数中return sizeof(数组名)结果是4
  • 一般地址总线总是按照对齐后的地址来访问的。假如你想得到0x00000001开始的4字节内容,系统首先需要以0x00000000读4字节,从中取得3字节,然后再用0x00000004作为开始地址,获得下一个4字节

总之记住四条规则

  1. 地址一般从0开始
  2. 元素存放的位置一定会在自己大小的整数倍上开始
  3. 检查计算出的存储单元是否为所有元素中最宽的元素长度的整数倍。若是,则结束;否则,将其补齐为它的整数倍
  4. 当结构体内的元素的长度都小于处理器位数时,以结构体中最长数据元素为对其单位。否则以处理器位数作为对齐单位

与strlen的区别

区别:

  • strlen的参数只能是char*,而且必须是‘\0’结尾的(由于其实现是计算‘\0’以前的字符个数),而且它是一个函数。sizeof是一个关键字。
  • sizeof操作符的结果类型是size_t,在头文件中typedef为unsigned int类型
  • sizeof在编译的时候就能计算出来,而strlen要在运行时才能确定,主要用来计算字符串长度

位域

位域基本说明

struct 位域结构体名 {
位域列表;
};
其中,位域列表为:类型说明符 位域名 : 长度;
例如:
在这里插入图片描述

  1. 无名位域:位域可以没有名字,只用作填充或调整位置。无名的位域是不能使用的宽度为0的一个未命名位域强制下一位域对齐到其下一type位域的边界
    在这里插入图片描述
  2. 位域的长度不能大于其类型说明符中指定类型的固有长度。比如int类型的位域长度不能超过32(bit),char类型的位域长度不能超过8(bit)
  3. 取地址运算符**(&)不能作用于位域**,任何指针都无法指向类的位域
  4. 位域的类型必须是整型或枚举类型,带符号类型中的位域的行为将因具体实现而定

位域的内存布局

refer to

  1. 整个位域结构体的总大小也是最宽基本类型成员大小的整数倍,与常规结构体一致。这说明位域本质上就是结构体
  2. 如果相邻位域字段的类型相同,且其声明的位宽长度之和小于此类型大小,则后面的位域字段将紧邻前一个字段存储,直到不能容纳为止
    在这里插入图片描述
  3. 如果相邻位域字段的类型相同,且其声明的位宽长度之和大于此类型大小,则后面的位域字段从新的存储单元开始,其起始偏移量为类型大小的整数倍
    在这里插入图片描述
    在这里插入图片描述
  4. 如果相邻位域字段的类型不同,则各编译器实现有差异,一般另起一个
    在这里插入图片描述
  5. 如果位域字段之间穿插着非位域字段,则不进行压缩
    在这里插入图片描述
  6. 当使用有符号类型来定义位域,且无意中使用到了正负特性时,就有问题了。所以一般声明成无符号类型
    在这里插入图片描述

关键字volatile

  1. 一个定义为volatile的变量volatile int i = 10;说明这个变量可能会被意想不到的改变,这样优化器在用到这个变量时必须每次都重新从它所在的内存中读取这个变量的值而不是直接使用保存在寄存器里的备份

例子1:返回指针指向值的平方。编译器可能将左边的代码变成右边的代码,这样有可能结果就不是一个准确的值了
在这里插入图片描述
而如果你想得到正确的结果,应当
在这里插入图片描述

  1. const可以是volatile。如只读的状态寄存器,它是const是因为程序不应该试图去修改它
  2. 指针可以是volatile。如一个中断服务子程序修改一个指向buffer的指针。

extern “C”

原因:cpp支持函数重载和c不支持。函数被cpp编译之后在库中的名字和c的不同,在库中就找不到该符号了
作用:让cpp编译器将extern "C"声明的代码当做C语言来处理。避免cpp因符号修饰导致代码不能和c语言库中的符号进行链接的问题
说明:

  1. 被extern限定的函数或变量是extern类型的,表示该变量或者函数在其他文件中进行了定义
  2. extern "C"修饰的变量和函数是按照C语言方式编译和链接

使用:
在这里插入图片描述


节省空间的联合体union

说明:

  • 一个union可以有多个数据成员,但是任意时刻只有一个数据成员可以有值。当某个成员被赋值后其他成员编程未定义状态。故联合体的大小是其中最大的数据成员的类型所占字节数

特点:

  • 与struct一样,默认的访问控制符为public
  • 可以含有构造函数和析构函数
  • 不能含有引用类型的成员
  • 不能继承自其他类,也不能作为基类派生出其他类(无继承
  • 不能含有虚函数(无多态

拓展:

匿名union

  • 不能包含protected成员或private成员
  • 匿名union定义所在的作用域可以直接访问union成员
  • 全局匿名union必须是静态的
    在这里插入图片描述

关键字explicit

关键字explicit修饰的构造函数可用来防止隐式转换
在这里插入图片描述


友元

提出原因:

  • 类的封装和隐藏特性导致只有类的成员函数才能访问私有成员,而程序中的其它成员是无法访问私有成员的。非成员函数可以通过类的对象访问类中的公有成员。但如果将数据成员定义为公有的,就破坏了隐藏的特性。

友元的特性:

  • 定义在类的外部,但是需要在类内用friend关键字声明
  • 友元不是成员函数,但是可以访问类的私有成员
  • 友元提高了程序运行效率,但破坏了类的封装性和隐藏性
  • 友元关系是单向的,若类B是类A的友元(在A中声明friend class B;),但类A不一定是类B的友元(取决于类B中是否声明了friend class A;)
  • 友元关系不具有传递性,若类B是类A的友元,类C是类B的友元,则类C不一定是类A的友元
  • 友元关系不能继承,基类的友元对派生类的成员没有特殊的访问权限

using声明

一条using声明using namespace_name::name;一次只引入命名空间的一个成员。告诉我们该声明作用域内的名字到底是哪个


::作用域解析符

  1. 访问全局变量(::name
  2. 访问类成员变量(class::name),此时一般是static的成员变量
  3. 访问命名空间内的成员(namespace::name

enum枚举类型

  1. 重定义及其解决方法
    在这里插入图片描述
  2. 起始值
    在这里插入图片描述

auto

需要把表达式的值赋给变量,需要在声明变量的时候清楚的知道变量是什么类型。然而做到这一点并非那么容易(特别是模板中)。auto 让编译器通过初始值来进行类型推演,从而获得定义变量的类型,所以说 auto 定义的变量必须有初始值。

使用注意:

  • 使用一个语句声明多个变量时,该语句所有变量的初始基本数据类型都必须是一样的
    auto sz = 0, pi = 3.14;//错误,两个变量的类型不一样

  • auto一般会忽略掉顶层const(decltype不会)但底层const会被保留下来
    在这里插入图片描述
    如果你希望推断出auto类型是一个顶层的const,需要明确指出:const auto f = ci;,否则f就是int类型的

  • auto会忽略引用
    在这里插入图片描述


关键字deltype

有时希望从表达式中推断出要定义变量的类型,但却不想用表达式的值去初始化变量。
还有可能是函数的返回类型为某表达式的值的类型。在这些时候auto显得就无力了

  1. 选择并返回操作数的数据类型。编译器分析表达式并得到它的类型,并不实际计算表达式的值

一些例子:
在这里插入图片描述

  1. 尾置返回类型,由于我们使用模板时我们还不知道返回类型,这样就不能使用auto

例子:
在这里插入图片描述


左值和右值

左值

既能够出现在等号左边也能出现在等号右边的变量或表达式(可以取地址的、有名字的

  • 表示的是对象的身份

右值

只能够出现在等号右边的变量或表达式(不能取地址的、没有名字的,通常不能被赋值

  • 表示的是对象的值

区别:左值持久,右值短暂

  • 左值有持久的状态,在多条代码中都能使用。如obj、*ptr、ptr[index]、++a,对这些表达式取地址都是合法的。
  • 右值要么是字面常量,要么是在表达式求值过程中创建的临时对象,在表达式结束后就消亡了。如233、a+b、string::("dzh")、a++,对这些表达式取地址都是非法的。

例子:
a++:先使用a的值,再加1,返回运算之前的a,是a的副本,无法被赋值,于是作为左值
在这里插入图片描述
++a:先加1,再使用a。返回的结果和运算后的一样,是真实的a,可以重新赋值,于是作为左值
在这里插入图片描述


左值引用和右值引用

参看此处

左值引用

  • 常规引用

右值引用

  • 必须绑定到右值的引用。通过&&而不是&来获得右值的引用。

左值引用和右值引用的使用范围

在这里插入图片描述
特别的:

  • 可以将一个const的左值引用或一个右值引用绑定到这类表达式上。如const int& i = 2;
  • 不能将一个右值引用直接绑定到一个左值上

例子:
在这里插入图片描述
在这里插入图片描述

右值引用的目的

可以实现转移语义精确传递

  • 消除两个对象交互时不必要的对象拷贝,节省运算存储资源,提高效率
  • 能够简洁明确地定义泛型函数

转移语义

将资源(堆,系统对象等)的状态或所有权从一个对象转移到另一个对象,避免不必要的临时对象的创建、拷贝和销毁,提高程序性能。只是转移,没有内存的搬迁或内存拷贝。如图是深拷贝和std::move的区别通过std::move可以避免不必要的拷贝,提高程序性能,将对象的状态或所有权从一个对象转移到另一个对象,只是转移,没有内存的搬迁或内存拷贝。如图是深拷贝和std::move的区别
在这里插入图片描述
右值引用只能绑定到一个临时对象、将要销毁的对象
于是可以自由地将一个右值引用的资源“移动”到另一个对象中。
举例:比如一个类对象中有一些指针或是动态数组,在对象的赋值和拷贝时便不需要拷贝这些资源
在这里插入图片描述
于是,可以直接使用临时对象已经申请的资源,这样既节省了资源,又节省了资源申请和释放的时间。这就是转移语义的目的
在这里插入图片描述

精确传递

适用于:将一组参数原封不动(指左值/右值和const/non-const)地传递给另一个函数。在泛型编程中,这样的需求非常普遍


std::move

它是一个函数模板,通过引用折叠,其参数可以与任何类型的实参匹配
目的:
都知道不能直接将一个右值引用绑定到一个左值上,而所有命名对象都只能是左值引用
如果已知一个命名对象不再被使用而想对它调用参数为右值引用的函数(如转移构造函数和转移赋值函数),
也就是要把一个左值引用当做右值引用来使用,这个时候就可以通过move获得一个绑定到左值上的右值引用

例子1:std::move以后,不能再使用移后源对象的值r1,只能使用r3
在这里插入图片描述
例子2:通过std::move,swap 函数避免了 3 次不必要的拷贝操作
在这里插入图片描述


成员初始化列表

更高效:

  • 少了一个默认构造函数的调用,在private中声明非内置类型时会调用默认构造函数
    在这里插入图片描述

有些场合必须使用初始化列表:

  • 常量成员:因为常量只能初始化不能赋值,所以必须在初始化列表中
  • 引用类型:引用必须在定义时初始化而且不能重新赋值
  • 没有默认构造函数的类类型(只要显式定义了类的构造函数,编译器不再提供默认构造函数):因为使用初始化列表可以不必调用默认构造函数来初始化,而是直接调用拷贝构造函数初始化

多态

  • 多态,即多种状态,在面向对象语言中,接口的多种不同的实现方式即为多态
  • C++多态有两种:静态多态(早绑定,函数重载实现),动态多态(晚绑定,虚函数实现)
  • 多态是以封装和继承为基础的

虚析构函数

功能:
为了解决基类的指针指向派生类对象,并用基类的指针删除派生类对象
在这里插入图片描述


抽象类、接口类、聚合类

  • 抽象类:含有纯虚函数的类
  • 接口类:仅含有纯虚函数的抽象类
  • 聚合类:用户可以直接访问其成员,并且具有特殊的初始化语法形式。满足如下特点:
    • 所有成员都是 public
    • 没有有定于任何构造函数
    • 没有类内初始化
    • 没有基类,也没有 virtual 函数

内存分配和管理

malloc、calloc、realloc、alloca

  • malloc:在堆中分配指定字节数的内存。申请到的内存中的初始值不确定
    在这里插入图片描述
  • calloc:为指定长度的对象,分配能容纳其指定个数的内存
    申请到的内存的每一位(bit)都初始化为0(与malloc的区别)
    在这里插入图片描述
  • realloc:更改以前分配的内存长度。返回的指针可能指的是另一个地址
    增加长度时,可能要将以前分配区的内容移到另一个足够大的区域,新增区域内的初始值则不确定
    减少长度时,那一部分的数据会丢失
    在这里插入图片描述
  • alloca:在栈上申请内存程序出栈时,自动释放内存。不具有可移植性,在没有传统堆栈的机器上很难实现

new和delete

  • new / new[]:完成两件事,先底层调用 malloc 分了配内存,然后调用构造函数(创建对象)
  • delete/delete[]:也完成两件事,先调用析构函数(清理资源),然后底层调用 free 释放空间
  • new 在申请内存时会自动计算所需字节数,而 malloc 则需我们自己输入申请内存空间的字节数。

注意:一定要记得在适当的时候delete掉避免内存泄漏
在这里插入图片描述

内存耗尽与定位new

一旦一个程序用光了它所有可用的内存,new表达式就会失败。
默认情况下,若new不能分配所要求的内存空间,会抛出一个bad_alloc异常
在这里插入图片描述

定位new允许我们向new传递额外的参数

  • new (palce_address) type
  • new (palce_address) type (initializers)
  • new (palce_address) type [size]
  • new (palce_address) type [size] { braced initializer list }

delete this是否合法?

是合法的,但:

  • 必须保证this对象是new分配出来的,但不是new[]
  • 必须保证delete this后不能再出现或使用this了

智能指针

shared_ptr

实现共享式拥有。多个智能指针指向相同对象,对象的最后一个拥有者负责销毁对象,并清理与该对象相关的资源。

  1. 初始化
    在这里插入图片描述
  2. shared_ptr的拷贝和赋值
    在这里插入图片描述
  3. 每个shared_ptr都有一个关联的引用计数。当一个shared_ptr的引用计数变为0,它会自动释放自己管理的对象
    引用计数递增:1)用一个shared_ptr初始化另一个。 2)将它作为参数传递给一个函数。 3)作为函数的返回值
    引用计数递减:1)给shared_ptr赋予一个新值。 2)shared_ptr被销毁(例如一个局部的shared_ptr离开其作用域)

unique_ptr

实现独占式拥有或严格拥有。保证同一时间内只有一个智能指针可以指向该对象。可以移交拥有权。
一旦拥有者被销毁或开始拥有另一个对象,先前拥有的那个对象就会被销毁,其相应的资源也会被释放。

  1. 初始化,它就没有make_shared
    在这里插入图片描述
  2. 由于一个unique_ptr拥有它指向的对象,因此不支持拷贝和赋值,但例外是可以拷贝或赋值一个将要被销毁的unique_ptr
    在这里插入图片描述
  3. 虽然不能拷贝或赋值unique_ptr,但可以通过release或reset将指针所有权从一个unique_ptr转移给另一个unique_ptr
    在这里插入图片描述

weak_ptr

允许共享但不拥有某对象,weak_ptr指向由一个shared_ptr管理的对象。

  1. 初始化,要用一个shared_ptr来初始化它,将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数
    在这里插入图片描述
  2. 最后一个指向对象的shared_ptr被销毁,对象就会被释放。
    即便有weak_ptr指向对象,weak_ptr指向的对象可能不存在,于是不能用weak_ptr直接访问对象,必须使用lock
    lock检查weak_ptr指向的对象是否存在,若存在则返回shared_ptr
    在这里插入图片描述

auto_ptr(C++98,被C++11弃用)

与unique_ptr的区别:

  • auto_ptr 可以赋值拷贝,复制拷贝后所有权转移;unqiue_ptr 无拷贝赋值语义,但实现了move 语义;
  • auto_ptr 对象不能管理数组(析构调用 delete);unique_ptr 可以管理数组(析构调用 delete[] );

强制类型转换符

static_cast

  • 用于非多态类型的转换
  • 不执行运行时类型检查(转换安全性不如 dynamic_cast)
  • 通常用于转换数值数据类型(如 float -> int)
  • 可以在整个类层次结构中移动指针,子类转化为父类安全(向上转换,向上转换是一种隐式转换。),父类转化为子类不安全(因为子类可能有不在父类的字段或方法)

const_cast

  • 用于改变运算对象的底层const、volatile 和 __unaligned 特性(如将 const int 类型转换为 int 类型 )
    在这里插入图片描述
  • 只有const_cast能改变表达式的常量属性

reinterpret_cast

  • 为运算对象的位模式提供简单的重新解释
    在这里插入图片描述
  • 滥用 reinterpret_cast 运算符可能很容易带来风险。 除非所需转换本身是低级别的,否则应使用其他强制转换运算符之一。
  • 允许将任何指针转换为任何其他指针类型(如 char* 到 int* 或 One_class* 到 Unrelated_class* 之类的转换,但其本身并不安全)
  • 也允许将任何整数类型转换为任何指针类型以及反向转换。
  • reinterpret_cast 运算符不能丢掉 const、volatile 或 __unaligned 特性
  • reinterpret_cast 的一个实际用途是在哈希函数中,即通过让两个不同的值几乎不以相同的索引结尾的方式将值映射到索引

dynamic_cast

  • 用于多态类型的转换,若源和目标类型没有继承关系则报错
  • 执行行运行时类型检查
  • 只适用于指针或引用
  • 对不明确的指针的转换将失败(返回 nullptr),但不引发异常
  • 可以在整个类层次结构中移动指针,包括向上转换(其实没必要,用虚函数可以实现)、安全的向下转换(即基类指针/引用->派生类指针/引用)。

使用形式:
在这里插入图片描述
其中,type必须是类类型,并且通常情况下该类型要有虚函数
e的类型必须满足以下三个条件中其中一个:

  • e的类型是目标type的公有派生类
  • e的类型是目标type的公有基类
  • e的类型就是目标type的类型

否则,转换失败。

  • 如果转换目标是指针类型且失败,结果为0
    在这里插入图片描述
  • 如果转换目标是引用类型且失败,抛出bad_cast异常(由于没有空引用不能返回0)
    在这里插入图片描述

运行时类型信息RTTI

dynamic_cast运算符

用于将基类的指针/引用安全地转换成派生类的指针/引用

typeid运算符

  • 用于返回表达式的类型。操作的结果是一个type_info常量对象的引用
  • type_info类重载了==!=
  • 含有一个name成员函数返回一个C风格字符串,表示对象类型的名字
  • 无法定义或拷贝type_info类型的对象,也不能赋值。创建该对象的唯一方式是typeid
  • typeid允许在运行时确定对象的类型。是否需要运行时检查决定了表达式是否会被求值。只有当类型含虚函数时,编译器才回对表达式求值。如果类型不含有虚函数,则typeid返回表达式的静态类型,编译器无需对表达式求值也能知道其静态类型
  • 若想通过基类的指针获得派生类的数据类型,基类必须带有虚函数

例子
在这里插入图片描述
在这里插入图片描述

展开阅读全文

没有更多推荐了,返回首页