c++面试题集锦
c++基础
C语言和C++有什么区别?
- 面向对象编程(OOP):
- C语言是一种过程式编程语言,不支持面向对象编程。
- C++是一种支持面向对象编程的语言,它提供了类、对象、继承、多态和封装等特性。
- 标准库:
- C语言的标准库相对较小,主要包括输入输出、字符串处理、数学计算等基本功能。
- C++的标准库更为丰富,除了包含C语言的标准库外,还包括STL(标准模板库),提供了向量、列表、队列、栈等数据结构和算法。
- 类型检查:
- C语言的类型检查较为宽松。
- C++的类型检查更为严格,例如,C++中函数重载和构造函数的概念都需要严格的类型检查。
- 函数重载:
- C语言不支持函数重载,即不能有同名函数。
- C++支持函数重载,可以在同一个作用域内定义多个同名函数,只要它们的参数列表不同即可。
- 引用:
- C语言中没有引用的概念,只能使用指针。
- C++引入了引用的概念,引用可以看作是变量的别名,使用起来比指针更简单、更安全。
- 异常处理:
- C语言通常使用返回值和错误码来处理错误。
- C++提供了异常处理机制,可以使用try、catch和throw关键字来处理运行时错误。
- 构造函数和析构函数:
- C语言中没有构造函数和析构函数的概念。
- C++中的类有构造函数和析构函数,用于对象的初始化和清理。
- 模板:
- C语言不支持模板。
- C++支持模板,可以创建通用函数和类,用于不同数据类型的操作。
总的来说,C++可以看作是C语言的超集,它保留了C语言的核心语法和特性,同时增加了面向对象编程和其他现代编程特性。
特性 | C语言 | C++ |
---|---|---|
内存管理 | 手动管理内存,使用malloc 、free 等函数 | 自动管理内存,提供new 、delete 等运算符 |
对象和类 | 无对象和类概念 | 支持面向对象编程,有类和对象的概念 |
继承 | 无继承机制 | 支持单继承和多继承 |
函数重载 | 无函数重载 | 支持函数重载 |
模板 | 无模板 | 支持模板编程 |
异常处理 | 无异常处理机制 | 支持异常处理 |
运算符重载 | 无运算符重载 | 支持运算符重载 |
构造函数和析构函数 | 无构造函数和析构函数 | 支持构造函数和析构函数 |
类型安全 | 相对较低,易出现类型错误 | 较高,编译时进行类型检查 |
库支持 | 标准库较简单 | 标准库更丰富,支持STL等 |
编译器支持 | 几乎所有编译器都支持C语言 | 部分编译器不支持或支持不完善 |
c++的struct和class有什么区别?
- 默认访问权限:
- 在C++中,
struct
的成员默认为public
。 - 在C++中,
class
的成员默认为private
。
- 在C++中,
- 继承时的默认访问权限:
struct
继承时默认为public
继承。class
继承时默认为private
继承。
- 传统用法:
- 在C++中,
struct
通常用于数据封装,但也可以包含成员函数。 - 在C++中,
class
通常用于表示具有数据和行为的对象。
- 在C++中,
c语言的struct和c++的struct有什么区别?
- 成员默认访问权限:
- 在C语言中,
struct
的成员默认是公有的(public),可以在结构体外部直接访问。 - 在C++中,
struct
的成员默认也是公有的,但C++支持访问修饰符(public
、private
、protected
),允许你显式地指定成员的访问权限。
- 在C语言中,
- 包含成员函数:
- 在C语言中,
struct
只能包含数据成员,不能包含成员函数。 - 在C++中,
struct
可以包含成员函数,与class
几乎一样,struct
在C++中实际上是一种轻量级的类。
- 在C语言中,
- 构造函数和析构函数:
- C语言中的
struct
不支持构造函数和析构函数。 - C++中的
struct
可以定义构造函数和析构函数,用于对象的初始化和清理。
- C语言中的
- 继承:
- C语言中的
struct
不支持继承。 - C++中的
struct
可以继承其他struct
或class
,并且可以添加额外的成员。
- C语言中的
- 模板:
- C语言中的
struct
不支持模板。 - C++中的
struct
可以作为模板使用,创建可以与任何类型一起工作的通用结构体。
- C语言中的
- 默认的继承访问权限:
- C语言没有继承的概念,因此也就没有默认的继承访问权限。
- 在C++中,
struct
继承时默认的继承访问权限是公有的(public),而class
默认是私有的(private)。
总的来说,C++中的struct
在功能上更接近于class
,是C++面向对象编程的一部分。而C语言中的struct
仅用于定义数据结构。
特性/语言元素 | C语言 struct | C++ struct | C++ class |
---|---|---|---|
默认成员访问权限 | 公有 (public) | 公有 (public) | 私有 (private) |
成员函数 | 不支持 | 支持 | 支持 |
构造函数/析构函数 | 不支持 | 支持 | 支持 |
继承 | 不支持 | 支持 | 支持 |
多态 | 不支持 | 支持 | 支持 |
模板 | 不支持 | 支持 | 支持 |
访问修饰符 | 不支持 | 支持 (public , private , protected ) | 支持 (public , private , protected ) |
默认继承访问权限 | 不适用 | 公有 (public) | 私有 (private) |
extern "C"的作用?
在C++中,extern "C"
是一个链接规范(linkage specification),它告诉C++编译器以C语言的规则来处理特定的函数或变量声明。这主要用于以下几种情况:
- C和C++混合编程: 当一个项目同时使用了C和C++代码时,
extern "C"
允许C++代码调用C语言编写的函数。由于C++支持函数重载,它会在编译生成的符号中包含函数的参数类型信息,而C语言则不会。因此,如果不使用extern "C"
,C++编译器可能会生成与C编译器不同的函数名,导致链接时无法找到对应的C函数。
函数重载和覆盖有什么区别?
特性/概念 | 函数重载(Overloading) | 函数覆盖(Overriding) |
---|---|---|
定义 | 同一个作用域内,同名函数具有不同的参数列表。 | 子类中有一个与父类同名、同参数列表的函数。 |
作用域 | 通常在同一个类或命名空间内。 | 发生在继承关系中的子类和父类之间。 |
参数列表 | 必须不同。 | 必须相同。 |
返回类型 | 可以不同。 | 通常应相同或为子类类型(covariant return type)。 |
虚函数 | 不是必须的。 | 通常涉及虚函数(在父类中声明为虚函数)。 |
多态性 | 与多态性无关。 | 实现多态性的关键机制。 |
调用方式 | 编译器根据参数列表决定调用哪个函数。 | 通过指向父类的指针或引用调用子类的函数。 |
示例 | void print(int value); void print(double value); | class Base { virtual void display(); }; class Derived : public Base { void display() override; }; |
了解RAII吗?介绍一下?
RAII(Resource Acquisition Is Initialization,资源获取即初始化)是C++语言中管理资源(如内存、文件句柄、网络连接等)的一种惯用法。它的核心思想是将资源的管理绑定到对象的生存周期上,利用对象的生命周期来控制资源的分配与释放,确保资源在使用完毕后能够被正确释放,从而避免资源泄漏。
RAII的特点:
- 构造函数获取资源:在对象的构造函数中进行资源的分配。
- 析构函数释放资源:在对象的析构函数中进行资源的释放。
- 对象作用域管理资源:通过对象的作用域来控制资源的生命周期。当对象创建时,资源被获取;当对象被销毁时(比如离开作用域),资源被自动释放。
RAII的优点:
- 防止资源泄漏:通过对象的生命周期管理资源,可以有效防止因为疏忽而忘记释放资源。
- 异常安全:即使在发生异常的情况下,对象的析构函数仍然会被调用,因此资源总是能够得到释放。
- 简化资源管理:将资源管理逻辑封装在对象的构造和析构中,简化了代码,提高了代码的可读性和可维护性。
类的大小怎么计算?
- 非虚成员:类中所有**非静态成员数据(不包括成员函数)**的大小总和。注意成员数据的对齐要求。
- 成员对齐:每个成员按照其类型的对齐要求在内存中进行对齐。
- 整体对齐:整个类的尺寸也要按照其最大成员的对齐要求进行对齐。
- 虚函数:
- 如果类中有虚函数,那么通常会有一个指向虚函数表的指针(vtable pointer)。
- 在大多数平台上,这个指针的大小是4字节(32位系统)或8字节(64位系统)。
- 继承:
- 如果类继承自基类,那么它的大小至少是基类大小加上自身成员的大小。
- 多重继承可能导致类拥有多个虚函数表指针。
- 虚继承:虚继承的类会有一个指向虚基类表(vbtable)的指针。
volatile关键字的作用
volatile
是C++中的一个关键字,它告诉编译器对象的值可能会以程序未明确指示的方式被改变。这通常发生在以下几种情况:
- 硬件寄存器:例如,一个状态寄存器,它的值可能会在任何时间被硬件改变。
- 中断服务程序:一个在主程序中定义的变量可能会被一个中断服务程序改变。
- 多线程:一个在主线程中定义的变量可能会被其他线程改变。
在默认情况下,编译器会对程序进行优化,以提高程序的运行效率。这些优化可能包括:缓存变量的值,删除未使用的变量,重新排序无关的指令等。然而,如果一个变量被声明为volatile
,那么编译器就不能对这个变量进行这些优化。每次访问volatile
变量时,都会直接从它的内存地址中读取值,而不是从寄存器或其他地方读取可能的缓存值。
这里有一个例子:
volatile int timer; // 假设这个变量会被硬件定时器改变void wait_for_timer() {int start = timer;while (timer == start) {// 等待定时器改变}
}
在这个例子中,如果timer
没有被声明为volatile
,那么编译器可能会认为在while
循环中timer
的值没有被改变,所以它会将timer
的值缓存起来,导致程序进入无限循环。如果timer
被声明为volatile
,那么编译器就会知道timer
的值可能会被改变,所以它每次都会从内存中读取timer
的值,而不是使用缓存值,这样程序就能正确地等待定时器改变。
了解各种强制类型转换的原理及使用?
类型转换 | 原理 | 用途 | 安全性 | 多态性 |
---|---|---|---|---|
static_cast | 1.编译时类型转换,用于非多态类型的转换,包括从子类型转换到父类型,从基础类型转换到派生类型,以及从一种指针类型转换到另一种指针类型。2.编译器会检查转换的有效性。 | 需要转换时,且转换是安全的,或者可以由编译器确定转换的安全性。 | 较高 | 无 |
dynamic_cast | 1.编译时类型转换,用于多态类型的转换,即从基类指针或引用转换到派生类指针或引用。2.如果转换的目标类型是指针,动态转换会检查转换是否有效。如果转换的目标类型是引用,则会进行运行时类型检查。 | 需要转换到多态类型的指针或引用时,且转换可能是不安全的。 | 较高 | 有 |
reinterpret_cast | 用于底层指针的转换,将一个指针直接转换为另一个指针,不进行任何有效性检查。这种转换不涉及任何类型的多态性,只是简单的指针转换。 | 需要底层指针之间的转换,或者需要绕过编译器的类型检查。 | 低 | 无 |
const_cast | 用于去除或添加const 属性。它可以用来修改指针或引用的const 属性,但不能改变类型。 | 需要修改指针或引用的const 属性,或者需要将一个const 指针转换为非const 指针。 | 较低 | 无 |
指针和引用有什么区别?什么情况下用指针,什么情况下用引用?
特性 | 指针 | 引用 |
---|---|---|
定义 | 指针是一个变量,存储另一个变量的内存地址。 | 引用是一个别名,绑定到一个变量,始终指向该变量。 |
使用 | 通过解引用指针(即通过指针访问它所指向的变量)来间接访问内存。 | 通过引用直接访问所绑定的变量的值。 |
初始化 | 可以不初始化,可以赋值为空指针NULL 。 | 必须初始化,一旦初始化就不能再指向其他变量。 |
多值性 | 一个指针可以指向多个不同的值,每次指向新的值时需要重新赋值。 | 一个引用始终指向一个变量,不能重新绑定。 |
安全性 | 需要手动管理内存,容易造成内存泄漏。 | 自动管理内存,不会造成内存泄漏。 |
大小 | 指针的大小取决于系统,通常是4个字节(32位系统)或8个字节(64位系统)。 | 与所绑定的变量大小相同。 |
- 指针:
- 当需要动态分配内存时,如使用
new
或malloc
。 - 当需要传递一个变量到函数,而不希望该函数改变变量的值时。
- 当需要返回一个函数的多个值时,可以通过指针实现。
- 当需要实现复杂的动态数据结构时,如链表、树等。
- 当需要动态分配内存时,如使用
- 引用:
- 当需要在函数内部修改参数值时,而不希望在函数外部保留修改后的值时。
- 当需要避免函数参数的拷贝操作,以提高效率时。
- 当需要定义函数的默认参数时。
new和malloc有什么区别?
特性 | new | malloc |
---|---|---|
返回类型 | 返回指针类型 | 返回指针类型 |
内存分配 | 可以分配对象内存,并自动调用对象的构造函数 | 只能分配普通内存,不调用构造函数 |
内存释放 | 需要使用delete 或delete[] 来释放内存 | 需要使用free 或free[] 来释放内存 |
类型安全 | 提供类型安全 | 不提供类型安全 |
错误处理 | 如果内存分配失败,抛出std::bad_alloc 异常 | 如果内存分配失败,返回NULL |
内存管理 | 通常分配在堆上 | 通常分配在堆上 |
适用范围 | 主要用于C++程序 | 主要用于C程序,但在C++中也可以使用 |
语法 | new T 或 new T[] | malloc(sizeof(T)) 或 malloc(sizeof(T[]) * n) |
智能指针支持 | 不支持智能指针 | 不支持智能指针 |
malloc的内存可以用delete释放吗?
不可以。malloc
返回的内存不能用delete
释放。malloc
和free
是C语言中的函数,用于动态内存分配和释放,而delete
是C++中的运算符,用于释放由new
分配的内存。
malloc出来20字节内存,为什么free不需要传入20呢,不会产生内存泄漏吗?
在C语言中,malloc
函数用于动态分配内存,它会返回一个指向分配内存块的指针。这个内存块的大小是在调用malloc
时指定的,例如,如果调用malloc(20)
,它将返回一个大小为20字节的内存块。
free
函数用于释放通过malloc
、calloc
或realloc
分配的内存。它不需要传入内存块的大小,因为free
函数会检查传入的指针是否确实是通过这些函数之一分配的。如果指针有效,free
会释放该内存块,并将其归还给系统。
这种设计可以避免内存泄漏,因为即使你不知道内存块的大小,free
也能够正确地释放它。它通过检查指针的地址来确定是否是它自己分配的内存,如果是,则释放;如果不是,则不做任何操作。
然而,这种设计也存在一个潜在的问题,即如果程序员错误地使用了一个不是通过malloc
、calloc
或realloc
分配的指针来调用free
,可能会导致程序崩溃或未定义的行为。因此,使用free
时需要确保传入的指针是正确的。
malloc、calloc、realloc的区别是什么?
函数 | 用途 | 返回值 | 内存初始化 | 潜在问题 |
---|---|---|---|---|
malloc | 动态分配内存 | 返回指向分配内存块的指针 | 内存块中的内容是未知的 | 释放时需要确保指针是通过malloc 分配的 |
calloc | 动态分配内存并初始化为0 | 返回指向分配内存块的指针 | 内存块中的所有字节都设置为0 | 释放时需要确保指针是通过calloc 分配的 |
realloc | 重新分配内存 | 返回指向新分配内存块的指针 | 可能移动旧内存块的内容 | 释放时需要确保指针是通过realloc 分配的 |
new[]和delete[]一定要配对使用吗?
是的,new[]
和delete[]
是一对配对的运算符,用于动态分配和释放数组内存。在C++中,使用new[]
动态分配数组内存时,必须确保使用delete[]
来释放该数组。不正确地使用new[]
和delete[]
会导致内存泄漏或其他未定义的行为。
以下是一些使用new[]
和delete[]
的正确做法:
-
使用
new[]
分配数组:int* arr = new int[10]; // 分配一个包含10个整数的数组
-
使用
delete[]
释放数组:delete[] arr; // 释放通过new[]分配的数组
-
避免混合使用
new[]
和delete
:// 错误的做法,会导致内存泄漏 int* arr = new int[10]; delete arr; // 应该使用delete[]来释放数组
-
避免重复释放:
// 错误的做法,会导致未定义的行为 int* arr = new int[10]; delete[] arr; delete[] arr; // 重复释放会导致未定义的行为
-
注意释放顺序:
// 错误的做法,可能导致数组中的内存被提前释放 int* arr1 = new int[10]; int* arr2 = new int[10]; delete[] arr1; // 应该先释放arr2 delete[] arr2;
正确的内存管理是确保程序正确运行的关键部分,因此务必正确使用new[]
和delete[]
来管理动态分配的数组内存。
C 语言的关键字 static 和 C++ 的关键字 static 有什么区别
- 在函数内部:在C和C++中,static关键字可用于函数内部变量。此时,此变量的生命周期将贯穿整个程序,即使函数执行结束,这个变量也不会被销毁。每次调用这个函数时,它都不会重新初始化。这可以用于实现一些需要保持状态的函数。
- 在函数外部或类内部:在C和C++中,
static
关键字可以用于全局变量或函数。此时,此变量或函数的作用域被限制在定义它的文件内,无法在其他文件中访问。这可以防止命名冲突或不必要的访问。 - 在类内部:只有C++支持此用法。在C++中,
static
关键字可以用于类的成员变量或成员函数。对于静态成员变量,无论创建多少个类的实例,都只有一份静态成员变量的副本。静态成员函数则可以直接通过类名调用,而无需创建类的实例。
C++中,static关键字有什么作用?
-
局部静态变量:
- 当
static
用于局部变量时,它使得变量的生命周期延长到程序运行结束,但变量的作用域仍然限制在定义它的函数或块内。 - 局部静态变量在程序第一次执行到其定义时初始化,并且只初始化一次。
- 它们在函数调用之间保持其值。
void function() {static int count = 0; // 初始化只发生一次count++;std::cout << count << std::endl; }
- 当
-
全局静态变量:
- 当
static
用于全局变量时,它限制了变量的链接属性,使得变量只能在其定义的文件内可见,即它具有内部链接属性。 - 全局静态变量在程序开始执行前初始化。
cpp
复制
static int globalVar = 10; // 只在定义它的文件中可见
- 当
-
静态成员变量:
- 在类中,
static
关键字用于定义静态成员变量,这些变量属于类而不是类的任何一个对象。 - 静态成员变量在程序的生命周期内只存在一份,无论创建了类的多少个对象。
- 静态成员变量必须在类外部定义和初始化。
class MyClass { public:static int staticVar; };int MyClass::staticVar = 0; // 定义和初始化
- 在类中,
-
静态成员函数:
- 在类中,
static
关键字也可以用来定义静态成员函数。 - 静态成员函数没有
this
指针,因此不能直接访问非静态成员。 - 静态成员函数可以通过类名直接调用,不需要类的对象。
class MyClass { public:static void staticFunc() {// 可以访问静态成员变量,但不能直接访问非静态成员} };MyClass::staticFunc(); // 调用静态成员函数
- 在类中,
-
静态函数:
- 在函数声明前使用
static
关键字,表示该函数具有内部链接属性,即它只能在其定义的文件内可见。
static void myFunction() {// 这个函数在定义它的文件之外不可见 }
- 在函数声明前使用
总结来说,static
关键字在C++中的作用包括控制变量的生命周期、可见性和链接属性,以及在类中定义属于整个类的变量和函数。
C++中,#define和const有什么区别?
特性/关键字 | #define | const |
---|---|---|
类型安全性 | 不检查类型,仅仅是文本替换 | 类型安全,会进行类型检查 |
内存分配 | 不分配内存,仅文本替换 | 分配内存,具有地址 |
调试 | 无法调试,因为预处理器处理后不存在 | 可以调试,因为它是编译时的常量 |
作用域 | 默认全局作用域,到文件结束都有效,不能定义局部作用域 | 限制在定义它的块或者文件中 |
静态链接和动态链接有什么区别?
特性/条件 | 静态链接 | 动态链接 |
---|---|---|
链接过程 | 在编译时完成,生成单一的可执行文件 | 在程序运行时完成,生成可执行文件和共享库 |
执行文件大小 | 通常较大,包含所有库代码 | 通常较小,不包含所有库代码 |
资源共享 | 不支持,每个可执行文件都有独立的库副本 | 支持,多个程序可以共享同一个库文件 |
程序启动速度 | 启动速度可能更快,无需动态链接 | 启动速度可能较慢,需要查找和加载库 |
类型 | 静态库(.lib, .a) | 动态库(.dll, .so, .dylib) |
变量的声明和定义有什么区别
声明是告诉编译器某个变量的存在,以及它的类型。声明并不分配存储空间。例如,外部变量的声明extern int a;
,这里只是告诉编译器有一个类型为int的变量a存在,具体的a在哪里定义的,编译器此时并不知道。
定义是声明的延伸,除了声明变量的存在和类型以外,还分配了存储空间。例如,int a;
就是一个定义,编译器在这里为a分配了足够的存储空间来存储一个整数。
typedef 和define 有什么区别
特性/条件 | typedef | #define |
---|---|---|
类型 | 类型别名机制 | 文本宏替换 |
作用域 | 可以是局部的也可以是全局的 | 默认全局,不能定义为局部 |
类型安全性 | 提供类型安全性,编译时类型检查 | 不提供类型安全性,仅预处理文本替换 |
参数化 | 不能直接用于创建带参数的类型别名 | 可以创建带参数的宏 |
解析时间 | 编译时解析 | 预处理时解析 |
用法 | 用来定义结构体、联合体、枚举等类型的别名 | 用来定义常量、宏函数、类型别名等 |
示例 | typedef int Length; | #define PI 3.14159 |
优势 | 提供更好的类型安全性和清晰的代码 | 灵活,可用于复杂的文本替换 |
劣势 | 不能创建带参数的别名 | 缺乏类型安全性,可能导致不可预期的错误 |
final和override关键字的作用
关键字 | 作用 |
---|---|
final | - 当用于类时,表示该类不能被继承。 |
- 当用于虚函数时,表示该函数不能在派生类中被覆盖。 | |
override | - 用于指示派生类中的成员函数打算覆盖其基类中的同名虚函数。如果基类中没有对应的可覆盖虚函数,编译器将报错。 |
// final 关键字示例
class Base final {// 这个类不能被继承
};class Base {
public:virtual void func() final; // 这个虚函数不能在派生类中被覆盖
};// override 关键字示例
class Base {
public:virtual void show() const;
};class Derived : public Base {
public:void show() const override; // 确保这是覆盖基类中的虚函数
};
宏定义和函数有何区别?
特性/概念 | 宏定义 | 函数 |
---|---|---|
定义方式 | 使用预处理器关键字 #define | 普通函数定义 |
执行时机 | 编译时(预处理阶段) | 运行时 |
参数检查 | 无类型检查,不进行参数匹配 | 有类型检查,参数类型必须匹配 |
返回值 | 不支持返回值的概念,但可以用逗号表达式 | 有明确的返回类型,可以返回值 |
调用方式 | 直接替换文本 | 通过函数调用栈 |
性能 | 通常更快,没有函数调用的开销 | 有函数调用的开销 |
作用域 | 通常全局有效,除非用 #undef 取消定义 | 受限于定义的作用域 |
参数求值 | 参数可能被多次求值,可能导致副作用 | 参数在函数调用时求值一次 |
代码长度 | 可能导致代码膨胀 | 不会导致代码膨胀 |
debug | 难以调试,因为预处理器展开后的代码不保留原始宏结构 | 可以正常调试 |
递归 | 不支持递归(C99 标准中宏可以模拟递归) | 支持递归 |
可变参数 | 可以使用可变参数宏 ##__VA_ARGS__ | 可以使用 ... 运算符实现可变参数函数 |
sizeof 和strlen 的区别?
区别点 | sizeof | strlen |
---|---|---|
定义 | 操作符,用于在编译时确定数据类型的大小 | 函数,用于在运行时确定字符串的长度 |
返回值类型 | size_t ,表示以字节为单位的大小 | size_t ,表示字符串中的字符数(不包括结尾的空字符 ‘\0’) |
参数 | 直接作用于数据类型或变量名 | 作用于以空字符结尾的字符数组或字符串指针 |
计算时机 | 编译时 | 运行时 |
包含空字符 | 如果作用于字符串,sizeof 会包括结尾的空字符 ‘\0’ | strlen 计算的长度不包括结尾的空字符 ‘\0’ |
作用于 | 任何数据类型或变量 | 只能作用于字符串 |
对数组的影响 | 如果作用于数组,会返回整个数组的大小,而不仅仅是第一个元素的大小 | 只计算数组中字符的数量,直到遇到第一个空字符 ‘\0’ |
简述strcpy、sprintf 与memcpy 的区别?
函数 | 用途 | 参数类型 | 复制行为 | 包含的结束符 | 安全性 |
---|---|---|---|---|---|
strcpy | 复制字符串(包括空字符 ‘\0’)到另一个字符串 | char *dest, const char *src | 字符串复制,逐字符 | 包括 ‘\0’ | 不安全,无边界检查 |
sprintf | 格式化字符串,并将其存储到字符串缓冲区中 | char *str, const char *format, ... | 格式化并复制字符串 | 包括 ‘\0’ | 不安全,无边界检查 |
memcpy | 在内存中复制指定数量的字节,可以复制任何类型的数据 | void *dest, const void *src, size_t num | 字节复制,按块 | 包括 ‘\0’ | 较安全,需指定大小 |
结构体可以直接赋值吗?
在C++中,结构体(struct
)可以直接赋值,但前提是该结构体没有成员函数、虚函数、或者指向同一结构体类型的非静态成员指针。如果结构体仅包含简单数据成员(例如整数、浮点数、字符数组等),那么可以直接赋值。
下面是一个简单的结构体赋值的例子:
#include <iostream>struct Point {int x;int y;
};int main() {Point p1 = {1, 2};Point p2;// 直接赋值p2 = p1;std::cout << "p2.x = " << p2.x << ", p2.y = " << p2.y << std::endl;return 0;
}
在这个例子中,p2
被直接赋值为 p1
的值,这是完全合法的。
但是,如果结构体包含以下内容,则不能直接赋值:
- 成员函数
- 虚函数
- 非静态成员指针,指向同一结构体类型
如果结构体包含这些元素,通常需要自定义复制构造函数和赋值运算符来处理这些复杂的情况。
例如,以下结构体不能直接赋值,因为它包含了一个指向自身类型的指针:
struct Node {int value;Node* next; // 指向相同类型的指针
};Node n1, n2;
n1.next = &n1; // n1 指向它自己// 错误的赋值,会导致浅拷贝问题
// n2 = n1; // 这将导致 n2.next 也指向 n1,这是不正确的
在上面的例子中,直接赋值会导致 n2.next
也指向 n1
,这通常不是我们想要的结果。正确的做法是定义复制构造函数和赋值运算符来创建深拷贝。
一个参数可以既是const又是volatile吗?
在C++中,一个参数可以同时被声明为const
和volatile
。这样的声明意味着该参数是一个常量(即不能被修改),同时它的值可能会在程序的控制之外被改变(例如,由于硬件或并发线程的修改)。
下面是一个示例,展示了如何声明一个既是const
又是volatile
的参数:
void readSensorData(const volatile int& sensorValue) {// 这里可以读取sensorValue的值,但不能修改它// 同时,我们假设sensorValue可能会在没有显式写操作的情况下改变
}
在这种情况下,sensorValue
是一个引用,它指向一个int
类型的值,该值既不会被readSensorData
函数修改(由于const
),也可能会在没有明确通知的情况下改变(由于volatile
)。
通常,const volatile
用于以下场景:
- 当你正在与硬件寄存器交互时,这些寄存器的内容可能会在没有程序干预的情况下改变(例如,一个表示传感器读数的寄存器)。
- 在多线程环境中,当多个线程可能会访问和修改同一个变量,而你不希望在自己的线程中修改这个变量,但需要确保每次读取都是最新的值。
全局变量和局部变量有什么区别?操作系统和编译器是怎么知道的?
特性 | 全局变量 | 局部变量 |
---|---|---|
作用域 | 整个程序或文件(除非被声明为静态) | 函数内部或代码块内部 |
生命周期 | 程序开始到结束 | 函数调用或代码块执行期间 |
初始化 | 默认初始化为0(非静态)或未定义(静态) | 未初始化,除非明确指定 |
存储 | 通常存储在数据段(如.bss或.data) | 存储在栈上 |
访问 | 可在程序的任何部分访问(通过适当的声明) | 只能在定义它们的函数或代码块内访问 |
可见性 | 可被程序中的所有函数访问(除非被隐藏或声明为静态) | 只能在定义它们的函数或代码块内可见 |
操作系统和编译器知道变量是全局的还是局部的,主要通过以下方式:
- 编译器:编译器在编译时会根据变量的声明位置确定它是全局变量还是局部变量。全局变量的声明在所有函数外部,局部变量的声明在函数内部。编译器也会根据这些信息生成相应的代码来创建和销毁局部变量,或者来访问全局变量。
- 操作系统:操作系统通常不关心变量是全局的还是局部的。操作系统管理的是进程和内存,而不是变量。当程序运行时,操作系统会为程序分配一片内存空间,程序如何使用这片内存空间(例如,哪部分用于全局变量,哪部分用于局部变量)完全取决于程序本身。这些信息通常在程序的可执行文件中,操作系统在加载可执行文件时会根据这些信息来设置内存。
例如,以下代码展示了全局变量和局部变量的区别:
// 全局变量
int globalVar = 0;void func() {// 局部变量int localVar = 1;// 在这里可以访问 globalVar 和 localVar
}int main() {// 在 main 函数中可以访问 globalVar// localVar 是 func 的局部变量,在这里不可见return 0;
}
在上述代码中,globalVar
是全局变量,可以在 main
函数和 func
函数中访问。而 localVar
是局部变量,只能在 func
函数内部访问。编译器通过代码分析和符号表来管理这些变量的作用域和生命周期。
C++中的指针和引用的区别?
特性 | 指针 | 引用 |
---|---|---|
定义 | 指针是一个变量,存储了另一个变量的地址 | 引用是一个变量的别名,不存储地址 |
初始化 | 必须在声明时初始化(除非是野指针) | 必须在声明时初始化 |
重新赋值 | 可以重新指向另一个变量 | 一旦初始化,就不能更改引用 |
空值 | 可以有空值(nullptr) | 不能有空值 |
操作符 | 使用 * 来解引用,使用 -> 来访问成员 | 使用 . 来访问成员,不需要解引用 |
多级 | 可以有指向指针的指针 | 没有多级引用的概念 |
内存占用 | 指针本身占用内存空间 | 引用不占用额外的内存空间 |
函数参数 | 可以传递指针的副本,也可以传递指针指向的对象的副本 | 总是传递引用所代表的对象的引用 |
传递空值 | 可以传递空指针给函数 | 不能传递空引用给函数 |
以下是具体的代码示例:
int main() {int a = 10;int b = 20;// 指针示例int* ptr = &a; // 指针初始化*ptr = 30; // 通过指针修改a的值ptr = &b; // 重新赋值指针// 引用示例int& ref = a; // 引用初始化ref = 40; // 通过引用修改a的值// ref = &b; // 错误:引用不能重新绑定到另一个对象return 0;
}
在上述代码中,指针 ptr
可以在声明后重新指向另一个变量 b
,而引用 ref
一旦初始化为变量 a
的引用,就不能再更改。
数组名和指针区别?
特性 | 数组名 | 指针 |
---|---|---|
类型 | 表示一组相同类型数据的集合 | 表示内存地址的变量 |
内存分配 | 在声明时分配固定大小的内存 | 需要显式分配或初始化 |
地址 | 代表数组首元素的地址 | 可以指向任何类型的变量的地址 |
可修改性 | 数组名通常是一个常量,不能被重新赋值 | 指针可以在运行时改变指向 |
下标访问 | 可以使用下标运算符 [] 来访问数组元素 | 可以使用解引用运算符 * 和指针算术来访问内存 |
sizeof 操作 | sizeof 运算符返回整个数组的大小(元素数量 * 元素大小) | sizeof 运算符返回指针本身的大小(通常是地址大小,如 4 或 8 字节) |
传递给函数 | 传递数组名时通常会退化为指针 | 显式传递指针 |
什么是智能指针?智能指针有什么作用?分为哪几种?各自有什么样的特点?
智能指针(Smart Pointer)是C++中的一种特殊类型的指针,它能够提供对普通指针的额外功能,例如自动内存管理、异常安全、所有权转移等。智能指针通常通过类模板实现,并且重载了指针的一些操作符,如 *
和 ->
,使得它们在行为上类似于普通指针。
智能指针的作用:
- 自动内存管理:智能指针可以自动释放所指向的对象,从而避免内存泄漏。
- 异常安全:智能指针可以在异常发生时自动释放资源,从而避免资源泄漏。
- 所有权管理:智能指针可以管理资源的所有权,防止多次删除同一资源。
常见的智能指针类型及其特点:
- std::auto_ptr(C++98,已废弃):
- 特点:实现了一个基本的智能指针,能够在对象超出作用域时自动释放资源。
- 缺点:不支持拷贝语义,拷贝操作会将所有权转移,导致原指针悬空。
- std::unique_ptr(C++11起):
- 特点:独占所指向的对象,同一时间只能有一个
unique_ptr
指向同一个对象。 - 优点:提供了更安全的内存管理,支持移动语义但不支持拷贝语义。
- 特殊用法:可以通过自定义删除器来指定释放资源的具体方式。
- 特点:独占所指向的对象,同一时间只能有一个
- std::shared_ptr(C++11起):
- 特点:共享所有权,多个
shared_ptr
可以指向同一个对象,并通过引用计数来管理对象的生命周期。 - 优点:支持拷贝和赋值操作,使得多个指针可以共享同一个对象。
- 缺点:可能引入性能开销,因为需要维护引用计数。
- 特点:共享所有权,多个
- std::weak_ptr(C++11起):
- 特点:用于解决
shared_ptr
可能产生的循环引用问题,它不增加引用计数。 - 优点:允许观察资源而不保持资源的所有权。
- 特殊用法:通常与
shared_ptr
配合使用,用于创建对资源的非拥有引用。
- 特点:用于解决
右值引用有什么作用?
- 提高性能:右值引用允许程序员更有效地利用资源,特别是在涉及临时对象时。通过右值引用,可以避免不必要的对象拷贝,从而提高程序的性能。
- 移动语义:右值引用是实现移动语义的关键。移动语义允许资源的所有权从一个对象转移到另一个对象,而不是复制资源。这在处理包含大量数据的对象(如字符串、容器、文件内容等)时特别有用。
- 返回大对象:函数可以返回临时对象的右值引用,这样可以在不进行复制的情况下返回大对象,比如从函数返回一个局部的大型
std::vector
对象。 - 完美转发:右值引用可以和模板结合使用来实现完美转发,即在函数模板中将参数不变地传递给其他函数。这意味着保持参数的左值或右值属性。
悬挂指针与野指针有什么区别?
特性/指针类型 | 悬挂指针 (Dangling Pointer) | 野指针 (Wild Pointer) |
---|---|---|
定义 | 指向已经释放的内存的指针 | 指向未知或未初始化内存的指针 |
初始化状态 | 之前指向已分配的合法内存 | 未初始化或指向非法内存地址 |
产生原因 | 内存被释放后没有重置指针 | 指针未初始化或越界访问 |
后果 | 可能访问到被重新分配的内存 | 访问未知的内存区域,行为不可预测 |
防范措施 | 释放内存后将指针设置为NULL 或nullptr | 始终初始化指针,避免越界访问 |
行为 | 内存释放后未及时更新导致的问题 | 从声明开始就可能存在风险 |
指针常量与常量指针区别
指针常量(Pointer to Constant)
- 定义:指针常量是指向常量数据的指针,意味着不能通过这个指针来修改它所指向的数据。
- 语法:
const Type* pointer;
- 特性:
- 数据不可修改:不能通过指针修改它所指向的数据。
- 指针可修改:指针本身可以指向其他地址。
常量指针(Constant Pointer)
- 定义:常量指针是指针本身是常量,意味着一旦指针被初始化指向某个地址后,就不能再改变它指向的地址。
- 语法:
Type* const pointer;
- 特性:
- 数据可修改:可以通过指针修改它所指向的数据。
- 指针不可修改:指针本身不能指向其他地址。
句柄和指针的区别和联系是什么?
对比项 | 句柄 | 指针 |
---|---|---|
定义 | 句柄是一种用于标识资源(如内存、文件、网络连接等)的抽象引用 | 指针是一种变量,用于存储另一个变量的内存地址 |
类型 | 句柄通常是整数或对象 | 指针有特定的类型,指向特定类型的变量 |
访问资源 | 句柄通过操作系统提供的接口间接访问资源 | 指针直接访问内存地址,操作更加灵活 |
安全性 | 句柄相对安全,操作系统可以对其进行权限控制 | 指针操作不当可能导致内存泄漏、野指针等安全问题 |
作用域 | 句柄的作用域通常受操作系统管理,跨进程传递时较为方便 | 指针的作用域通常在单个进程内,跨进程传递需要特殊处理 |
联系 | 句柄可以看作是一种特殊的指针,它们都是用于访问资源的引用 | 指针可以直接操作内存,而句柄则需要通过操作系统提供的接口来间接操作资源 |
对c++中shared_ptr,unique_ptr,weak_ptr,auto_ptr的理解
引自:https://www.zhihu.com/search?type=content&q=shared_ptr%2Cunique_ptr%2Cweak_ptr%2Cauto_ptr
智能指针的作用是管理一个指针,因为存在以下这种情况:申请的空间在函数结束时忘了释放,造成内存泄漏。使用智能指针可以很大程度上避免这个问题,因为智能指针就是一个类,当超出了类的作用域时,类会自动调用析构函数,析构函数会自动释放资源。所以只能指针的作用原理就是在函数结束时候自动释放内存空间,不需要手动释放内存空间。
智能指针是代理模式的具体运用,它使用 RAII 技术代理了裸指针,能够自动释放内存,无需程序干预。
如果指针是“独占”使用,就应该选择 unique_ptr,它为裸指针添加了很多限制,更加安全。
如果指针是“共享”使用,就应该选择 shared_ptr,它的功能完善,用法几乎和原始指针一样。
shared_ptr 有少量的管理成本,也会引发一些难以排查的错误,所以不要过度使用。
unique_ptr
unique_ptr 是最简单、最容易使用的一个智能指针,在声明的时候必须用模板参数指定类型。
unique_ptr 实现独占式拥有或严格拥有的概念,保证同一时间内只有一个智能指针可以指向该对象。它对于避免资源泄漏(例如“以 new 创建对象后因为发生异常而忘记调用 delete“)特别有用。
unique_ptr<int> ptr1(new int(10)); // 直接初始化
unique_ptr<int> ptr2 = new int(); // error,构造函数是 explicit
ptr1 += 1; // error,因为unique_ptr 实际上是个对象,所以不能对其进行指针移动
unique_ptr<int> ptr3;
ptr1 = ptr3; // 报错,编译器避免了ptr1不再指向有效数据的问题。
注意:如果确实想执行类似指针赋值的操作,要安全的重用这种指针,可给它赋新值。C++ 有一个标准库函数 std::move(),让你能够将一个 unique_ptr 赋值给另一个。
unique_ptr<string> ps1, ps2;
ps1 = demo("hello");
ps2 = move(ps1);
ps1 = demo("hhhhhh");
虽然 unique_ptr 名字叫指针,用起来也很像,但是它实际上并不是指针,而是一个对象。所以不能对他调用 detele,它会自动管理初始化的指针,在离开作用域时析构释放内存。
未初始化的 unique_ptr 表示空指针,对其进行使用会导致程序崩溃。
unique_ptr<int> ptr2;
*ptr2 = 2; // 导致崩溃,因为没有进行初始化,操作了空指针
一个对象只能被单个 unique_ptr 所引用,所以禁止拷贝,而且在向另一个 unique_ptr 赋值的时候,要特别留意,必须用 std::move() 函数显式地声明所有权转移。赋值操作之后,指针的所有权就被转走了,原来的 unique_ptr 变成了空指针,新的 unique_ptr 接替了管理权,保证了所有权的唯一性。不过还是尽量少用 unique_ptr 执行赋值操作,保证其自动化管理。
unique_ptr<int> ptr3(ptr1); // error,不允许拷贝
shared_ptr
shared_ptr 实现共享式拥有的概念。多个智能指针可以指向相同的对象,该对象和其相关的资源会在“最后一个引用被销毁”时候释放。
shared_ptr 通过引用计数的方式,实现安全共享,可以通过成员函数 use_count() 来查看资源的所有者个数。引用计数最开始时候是 1,表示只有一个使用者,如果发生拷贝赋值(共享)的时候,引用计数增加,发生析构的时候,引用计数减少,只有当引用计数减少到 0 的时候,它才会真正调用 delete 释放内存。
shared_ptr 是为了解决 auto_ptr 在对象所有权上的局限性 (auto_ptr 是独占的),在使用引用计数的机制上提供了可以共享所有权的智能指针。
成员函数:
use_count() 返回引用计数的个数;
unique 返回是否是独占所有权(use_count 为 1)
swap 交换两个 shared_ptr 对象(即交换所拥有的对象)
reset 放弃内部对象的所有权或拥有对象的变更,会引起原有对象的引用计数的减少。
get 返回内部对象(指针),由于已经重载了()方法,因此和直接使用对象是一样的,如 shared_ptr sp(new int(1)); sp 与 sp.get() 是等价的。
因为 shared_ptr 支持拷贝赋值,所以它可以在任何场合替代原始指针,而不再担心资源回收的问题。
shared_ptr<int> ptr1(new int(10)); // 直接初始化
shared_ptr<int> ptr3(ptr1); // 允许拷贝
shared_ptr 是线程安全的。
同一个 shared_ptr 被多个线程读是安全的,被多个线程写是不安全的,共享引用计数的不同的 shared 被多个线程写是安全的。
虽然 shared_ptr 更为智能,但是维护引用计数的存储和运算都是需要成本的,虽然因为 shared ptr 内部有比较好的优化,成本比较低。
shared_ptr 的引用计数也导致“循环引用”的问题,这在把 shared_ptr 作为类成员的时候最容易出现。
如果有容易产生“循环引用”的场合,可以考虑使用 weak_ptr,weak_ptr 是专门为了打破循环引用而设计,它只观察指针,不会增加引用计数,但是在需要的时候,可以调用成员函数 lock(),获取 shared_ptr。
weak_ptr
weak_ptr 是一种不控制对象生命周期的智能指针,它指向一个 shared_ptr 管理的对象。进行该对象的内存管理的是那个强引用的 shared_ptr。weak_ptr 只是提供了对管理对象的一个访问手段。
weak_ptr 设计的目的是为了配合 shared_ptr 而引入的一种智能指针来协助 shared_ptr 工作,它只可以从一个 shared_ptr 或另一个 weak_ptr 对象构造,它的构造和析构不会引起引用计数的增加或减少。
weak_ptr 是用来解决 shared_ptr 相互引用时的思索问题,如果说两个 shared_ptr 相互引用,那么这两个指针的引用计数永远不可能下降为 0,资源永远不会释放。它是对对象的一种弱引用,不会增加对象的技术引用,和 shared_ptr 之间可以相互转化,shared_ptr 可以直接复制它,它可以通过 lock 函数来获得 shared_ptr。
class B;
chass A;
{
public:shared_ptr<B> pb_;~A(){count<<"A delete";}
};class B
{
public:shared_ptr<A> pa_;~B(){count<<"B delete";}
};void fun()
{shared_ptr<B> pb(new B());shared_ptr<A> pa(new A());pb->pa_ = pa;pa->pb_ = pb;count<<pb.use_count()<<endl;count<<pa.use_count()<<endl;
}int main()
{fun();return 0;
}// 可以看到fun函数中pa,pb之间相互引用,两个资源的引用计数为2,当要跳出函数时,
// 智能指针pa,pb析构时两个资源引用计数会减1,但是两者引用计数还是为1,导致跳出函数时资源没有被释放(A B的析构函数没有被调用),
// 如果把其中一个改为weak_ptr就可以了,我们把A里面的shared_ptr pb; 改为 weak_ptr pb;
// 这样的话,资源B的引用开始就只有1,当pb析构时,B的计数变成0,B得到释放,B释放的同时也会使A的计数减1,
// 同时pa析构时使A的计数减1,那么A的计数为0,A得到释放。// 注意:
// 不能通过weak_ptr直接访问对象的方法,比如B对象中有一个方法print(),我们不能这样访问:pa->pb->print();
// 因为pb是一个weak_ptr,应该先把它转化为shared_ptr,shared_ptr p = pa->pb_.lock(); p->print();
auto_ptr
auto_ptr 被 C++11 弃用,原因是缺乏语言特性如“针对构造和赋值”的 std::move 语义,以及其他瑕疵。