以下是包含C语言中的struct
与C++中的struct
和class
区别的完整表格:
特性 | C语言中的struct |
C++中的struct |
C++中的class |
---|---|---|---|
默认成员访问权限 | 无(所有成员默认都是公开的) | public |
private |
继承的默认访问权限 | 不支持继承 | public |
private |
面向对象支持 | 不支持 | 支持(可以包含构造函数、析构函数、继承、多态等) | 支持(可以包含构造函数、析构函数、继承、多态等) |
用途 | 用于简单的数据结构,主要存储数据 | 通常用于简单的数据结构,主要存储数据 | 通常用于复杂对象,包含数据和方法,强调封装 |
语法差异 | 无成员函数,不能包含方法或构造函数 | 与class 相同,除了默认访问权限 |
与struct 相同,除了默认访问权限 |
在C++中,struct
和class
的主要区别在于默认的访问权限,除此之外,它们在其他方面非常相似。具体来说:
struct
的成员(变量和函数)是public的。class
的成员是private的。struct
时,继承的默认访问权限是public。class
时,继承的默认访问权限是private。struct
和class
几乎完全相同。它们都可以包含成员变量、成员函数、构造函数、析构函数、以及支持继承和多态。struct
中实现面向对象的特性,就像在class
中一样。// struct 默认成员是 public
struct MyStruct {
int x; // public
MyStruct() {} // public 构造函数
};
// class 默认成员是 private
class MyClass {
int y; // private
public:
MyClass() {} // public 构造函数
};
struct
vs class
在功能上,struct
和class
是等效的,区别主要是基于设计风格和习惯。
在面试中,针对头文件的包含顺序和双引号 ""
与尖括号 <>
的区别,可以简洁地回答如下:
头文件的包含顺序:
""
。""
或 <>
。<>
。双引号 ""
和尖括号 <>
的区别:
""
:优先在当前目录或用户指定目录中查找头文件,常用于自定义头文件。<>
:直接在系统目录或标准库目录中查找头文件,通常用于标准库或系统库。“在C中,结构体只能包含数据成员,不能有函数成员或构造函数,且没有访问控制,声明时需要使用struct
关键字。C++对结构体做了扩展,它支持成员函数、构造函数、访问控制,还可以继承和多态,使用方式类似类,声明时不需要struct
关键字。”
特性 | C语言中的结构体 | C++中的结构体 |
---|---|---|
成员类型 | 只能包含数据成员 | 可以包含数据成员和函数成员 |
访问控制 | 没有访问控制,所有成员都是公开的 | 支持public 、private 、protected ,默认public |
继承和多态 | 不支持 | 支持继承和多态 |
构造函数/析构函数 | 不支持 | 支持构造函数和析构函数 |
声明变量时是否需要struct |
需要使用struct 关键字 |
不需要struct 关键字 |
extern “C” :用于声明C语言函数,使得C++编译器以C的方式链接它们,避免C++编译器的符号修饰(name mangling)。C编译器没有符号修饰,函数名较为简单,而C++为了支持函数重载、模板等特性,对函数名进行符号修饰,因此需要extern "C"来保证与C代码的兼容。”
// 告诉C++编译器这是一个C函数
extern "C" void my_c_function(int a);
//多个C函数需要在C++中使用。
extern "C" {
void my_c_function1(int a);
void my_c_function2(double b);
}
“符号修饰是编译器为了支持C++中的函数重载、模板、命名空间等特性,对函数名、变量名进行编码,使得每个符号在编译生成的二进制文件中是唯一的。C++中使用符号修饰来区分同名函数或变量,而C语言不支持函数重载,因此不需要符号修饰。为了与C语言代码兼容,C++提供了extern "C"关键字来关闭符号修饰。”
“C++代码到可执行文件的过程包括四个阶段:预处理阶段处理宏、文件包含等;编译阶段将代码翻译为汇编代码;汇编阶段将汇编代码转为机器代码生成目标文件;链接阶段将目标文件和库文件组合,解析符号并生成最终的可执行文件。”
C++从代码到可执行二进制文件的生成过程可以分为四个主要阶段:预处理、编译、汇编和链接。每个阶段都对源代码进行不同的转换,最终生成可执行文件。
在这个阶段,编译器对源代码中的预处理指令进行处理,例如:
#include
指令,将头文件的内容插入到代码中。#if
、#ifdef
等条件预处理指令,决定哪些代码片段会被编译。g++ -E
或cl /E
查看预处理输出。在编译阶段,预处理后的C++代码会被编译器翻译成中间代码或汇编代码。具体步骤包括:
g++ -S
生成汇编代码。汇编器将编译器生成的汇编代码转换成机器代码(也称为目标代码,即二进制文件),这些机器代码可以被计算机处理器直接执行。每个源文件经过汇编后,生成一个目标文件(以.o
或.obj
为后缀)。
目标文件包含机器指令和一些符号表,但它还不是一个完整的可执行文件,因为它可能会依赖于其他目标文件或库文件。
工具:通常用g++ -c
生成目标文件。
链接器将多个目标文件以及需要的库文件(如标准库或第三方库)组合起来,生成一个完整的可执行文件。主要任务包括:
//此命令会将静态库`libmy_static_library.a`嵌入到`myprogram`中,生成自包含的可执行文件。
g++ -static -o myprogram main.cpp -lmy_static_library
//此命令生成的`myprogram`会在运行时动态加载库`libmy_dynamic_library.so`。
g++ -o myprogram main.cpp -lmy_dynamic_library
对比点 | 静态链接 | 动态链接 |
---|---|---|
库代码存储方式 | 库代码嵌入到可执行文件中 | 库代码在运行时动态加载 |
可执行文件大小 | 文件较大 | 文件较小 |
运行时依赖 | 不依赖外部库,独立运行 | 依赖外部库,运行时必须提供相应的库文件 |
内存占用 | 各程序独立加载库代码,内存占用较大 | 各程序可共享动态库,减少内存占用 |
性能 | 稍微好一些,因为不需要在运行时加载库 | 稍微差一些,因为运行时需要加载库 |
库更新的灵活性 | 需要重新编译程序 | 可以单独更新库,不需要重新编译程序 |
适用场景 | 稳定的、独立的环境,如嵌入式系统 | 程序运行环境可控,如服务器或桌面系统 |
g++
或ld
负责链接过程。#include
等预处理指令。以g++
为例,编译一个C++文件:
g++ -o myprogram main.cpp
这条命令会经过预处理、编译、汇编和链接,最终生成可执行文件myprogram
。
面试简要回答:
“static 关键字在 C++ 中有多种作用:
局部变量中:声明的变量在函数多次调用间保持其值。
全局变量和函数中:限制作用域,使其只在声明所在的文件中可见。
类中:static 成员变量和成员函数属于类本身,所有对象共享,可以不依赖对象实例直接访问。”
static 关键字有多种作用,具体取决于它用于变量、函数或类中的位置。它的作用包括控制变量的存储方式、生命周期、作用域以及类成员的共享属性。
数组和指针在C/C++中有密切的关系,数组名可以看作是指向数组首元素的常量指针,但数组和指针的用法和内存管理方式有明显区别。数组具有固定大小并且在声明时分配内存,而指针则更灵活,可以指向不同的内存位置并动态分配或释放内存。
区别点 | 数组 | 指针 |
---|---|---|
定义 | 数组是固定大小的、存储同类型元素的集合。 | 指针是一个变量,存储另一个变量的内存地址。 |
存储空间 | 数组在声明时分配固定大小的连续内存空间。 | 指针只占用存储地址的大小(例如4或8字节)。 |
修改大小 | 数组的大小在定义时固定,不能在运行时修改。 | 指针可以通过动态内存分配来改变指向的数据大小。 |
内存位置 | 数组名代表数组第一个元素的地址,数组本身不可移动。 | 指针可以通过赋值或运算改变指向不同的地址。 |
数组名 vs 指针 | 数组名是常量,不能进行赋值操作。 | 指针是一个变量,可以指向不同的内存位置。 |
访问方式 | 通过下标访问数组元素,arr[i] 。 |
通过指针的解引用访问,*(ptr + i) 。 |
关系 | 数组名是数组首元素的指针,但数组和指针类型不同。 | 指针可以指向数组的第一个元素,也可以指向任意内存。 |
类型推导 | 数组名的类型是数组类型,如int[10] 。 |
指针类型明确,如int* ,代表指向整数的指针。 |
内存分配方式 | 数组的内存是自动或静态分配的,编译时分配。 | 指针可以通过malloc 、new 等动态分配内存。 |
传递给函数的方式 | 数组传递给函数时,传递的是指向第一个元素的指针。 | 指针可以直接传递给函数,或者指向任意位置。 |
函数指针 是一种指向函数的指针,它指向的是函数,而不是变量。通过函数指针,我们可以像调用普通函数一样调用指针所指向的函数。函数指针允许程序根据不同的需求动态选择要调用的函数,使代码更加灵活和模块化。通过函数指针,可以实现回调机制、动态函数调用、函数表、多态等功能,使得代码更加模块化和可扩展。
定义函数指针的语法比较复杂,通常遵循以下格式:
返回类型 (*函数指针名)(参数类型列表);
返回类型
:函数指针指向的函数的返回类型。函数指针名
:指针变量的名称。参数类型列表
:函数指针指向的函数所需的参数列表类型。例如,定义一个指向返回 int
类型,且带有两个 int
类型参数的函数指针:
int (*funcPtr)(int, int);
这个 funcPtr
是一个指向带有两个 int
参数并返回 int
值的函数的指针。
可以将已定义的函数赋值给函数指针。函数名其实是指向该函数的指针,因此可以直接赋值。
int add(int a, int b) {
return a + b;
}
int (*funcPtr)(int, int) = add; // 将 add 函数的地址赋给函数指针
调用函数指针时,可以像调用普通函数一样使用,但通过指针来进行函数的调用。
int result = funcPtr(3, 5); // 等同于调用 add(3, 5)
std::cout << result; // 输出 8
也可以通过显式解引用函数指针来调用:
int result = (*funcPtr)(3, 5); // 也可以这样写
下面是一个完整的示例,展示如何定义、赋值和调用函数指针:
#include
int add(int a, int b) {
return a + b;
}
int subtract(int a, int b) {
return a - b;
}
int main() {
int (*operation)(int, int); // 声明函数指针
operation = add; // 将 add 函数赋值给函数指针
std::cout << "Add: " << operation(5, 3) << std::endl; // 输出 8
operation = subtract; // 将 subtract 函数赋值给函数指针
std::cout << "Subtract: " << operation(5, 3) << std::endl; // 输出 2
return 0;
}
在某些场合,可能希望某个函数在执行完某些任务后,执行一个额外的操作。这个操作由用户决定,使用函数指针传递可以实现这种灵活性。典型的例子是事件驱动程序设计中的回调函数,例如在信号处理或UI编程中。
void performOperation(int x, int y, int (*operation)(int, int)) {
std::cout << "Result: " << operation(x, y) << std::endl;
}
int main() {
performOperation(10, 5, add); // 回调 add 函数
performOperation(10, 5, subtract); // 回调 subtract 函数
return 0;
}
在某些情况下,程序在运行时根据条件来决定调用哪个函数。使用函数指针,可以在程序运行时动态选择合适的函数。
例如,实现简单的菜单系统:
void menu() {
int choice;
std::cout << "1. Add\n2. Subtract\n";
std::cout << "Enter your choice: ";
std::cin >> choice;
int (*operation)(int, int);
if (choice == 1) {
operation = add;
} else if (choice == 2) {
operation = subtract;
}
std::cout << "Result: " << operation(10, 5) << std::endl;
}
在编写编译器、解释器或模拟器时,通常会有不同的操作码对应不同的操作。使用函数指针表可以提高代码的灵活性和可维护性。
int (*operations[2])(int, int) = {add, subtract}; // 函数指针数组
std::cout << "Add result: " << operations[0](3, 2) << std::endl;
std::cout << "Subtract result: " << operations[1](3, 2) << std::endl;
在C++的面向对象编程中,函数指针的概念用于虚函数和多态的底层实现。虚函数表(V-Table)实际上是一个包含虚函数指针的表,允许程序在运行时根据对象类型动态调用合适的函数。
类型 | C++中的静态变量 |
---|---|
局部静态变量 | - 初始化时机:第一次调用函数时 - 生命周期:贯穿整个程序,直到程序结束 |
全局静态变量 | - 初始化时机:程序启动时,在 main() 之前 - 生命周期:程序开始到结束,作用域限于文件 |
类的静态成员变量 | - 初始化时机:程序启动时,在类外定义并初始化 - 生命周期:所有对象共享,程序结束前有效,C++17支持 inline 初始化 |
静态链接/动态链接影响 | 静态链接时静态变量在加载时初始化;动态链接库中的静态变量在库加载时初始化 |
C++17 支持 inline 静态变量 | 支持 inline 静态成员变量,允许在类内直接初始化 |
static
修饰,首次调用时初始化,之后保持值不变。static
修饰限定其作用范围在文件内,初始化在程序启动时完成。可以。因为在编译时对象就绑定了函数地址,和指针空不空没关系。
总结
野指针是指向无效内存区域的指针,可能引发未定义行为或程序崩溃。它并不为 nullptr,因此从表面上看像是有效的指针,但实际上它所指向的内存空间可能已经被释放、回收或从未正确初始化过。这类指针一旦被访问,可能会引发不可预测的行为,如程序崩溃、数据损坏等。
它通常由于未初始化的指针、释放后的指针继续使用、返回局部变量地址、多次释放等原因产生。
通过初始化指针为 nullptr、释放内存后将指针设为 nullptr、使用智能指针等方法,可以有效避免野指针问题。
变量类型 | 存储位置 | 生命周期 | 作用域 | 初始化 | 典型使用场景 |
---|---|---|---|---|---|
局部变量 | 栈区 | 函数调用时分配,函数返回时释放 | 函数或代码块内部 | 每次进入作用域时重新初始化 | 临时数据存储,避免全局污染,函数内部的计算 |
静态局部变量 | 静态数据区 | 程序运行期间(保留状态) | 函数或代码块内部 | 程序首次执行到时初始化 | 多次调用函数时需保存状态,缓存中间结果 |
全局变量 | 静态数据区 | 程序运行期间 | 文件内或全局(可跨文件共享) | 程序开始时初始化 | 跨函数共享数据,系统状态变量,全局配置 |
静态全局变量 | 静态数据区 | 程序运行期间 | 文件内部(仅限于声明所在文件) | 程序开始时初始化 | 文件内部共享数据,避免命名冲突和全局污染 |
内联函数和宏函数都是用于提高程序执行效率的手段,但它们的实现机制和使用场景存在明显的不同。以下是它们之间的主要区别:
区别点 | 内联函数 | 宏函数 |
---|---|---|
定义方式 | 使用 inline 关键字声明的函数 |
使用 #define 预处理器指令定义 |
作用机制 | 编译时通过函数调用替换为函数体代码 | 预处理阶段进行简单的文本替换 |
类型检查 | 编译器会进行类型检查,确保参数类型正确 | 无类型检查,只做简单的文本替换 |
调试支持 | 支持调试,可以在调试器中查看内联函数的调用堆栈 | 不支持调试,宏展开后很难追踪问题 |
参数求值 | 参数只被求值一次,正常按值传递 | 参数会被多次求值,可能导致副作用 |
作用域 | 受 C++ 作用域规则限制,遵循函数作用域规则 | 没有作用域的概念,全局可见,容易引发命名冲突 |
错误排查 | 编译时检查,错误更易排查 | 宏展开后的错误更难定位,因为仅在预处理阶段处理 |
扩展性 | 支持递归和复杂逻辑,且支持重载和模板 | 宏不支持递归、模板或重载功能,只能做简单替换 |
inline
,也不一定会内联。#define SQUARE(x) ((x) * (x))
中 x
可能被多次求值)。在 C++ 中,i++
和 ++i
都是对变量 i
进行自增操作(即将 i
的值加 1),但它们的工作机制和返回值有所不同。
运算符 | 操作顺序 | 返回值 | 效率 | 使用场景 |
---|---|---|---|---|
i++ |
先使用 i ,后自增 |
返回自增前的值 | 相对较慢,可能涉及临时对象创建 | 用在需要当前值的场景 |
++i |
先自增,后使用 i |
返回自增后的值 | 相对较快,不涉及临时对象的创建 | 用在仅需自增结果的场景 |
int
),性能差异不明显。i++
可能涉及拷贝和临时对象的创建,而 ++i
更高效。++i
。i++
。new
是 C++ 特有的操作符,不仅分配内存,还会调用构造函数初始化对象。它的底层实现是调用 operator new
分配内存,而 operator new
通常通过 malloc
或类似机制实现。malloc
是 C 语言的函数,它只负责分配内存,不会调用构造函数。new
和 malloc
都是用于动态内存分配的操作,但它们在使用方式、底层实现和功能上有很大的区别。以下是它们的区别以及各自的底层实现原理:
new
和 malloc
的区别特性 | new |
malloc |
---|---|---|
语言 | C++ 特有 | C 和 C++ 都可以使用 |
调用构造函数 | 会调用构造函数来初始化对象 | 只分配内存,不调用构造函数 |
返回类型 | 返回的是正确的对象类型(不需要类型转换) | 返回 void* ,需要强制类型转换 |
内存分配失败 | 抛出 std::bad_alloc 异常 |
返回 NULL ,需要手动检查错误 |
内存释放 | 使用 delete 释放内存,调用析构函数 |
使用 free() 释放内存,只释放内存,不调用析构函数 |
重载 | 可以通过重载 new 操作符进行自定义分配行为 |
不支持重载 |
分配数组 | 可以用 new[] 来分配数组 |
只能通过 malloc 分配连续内存,不处理对象数组 |
new
的底层实现原理在 C++ 中,new
操作符不仅仅是分配内存,它还负责调用构造函数来初始化对象。
步骤 1:调用 operator new
分配内存:
new
操作符时,它会首先调用全局的 operator new
函数来分配足够的内存。这个函数通常是由标准库实现,类似于 malloc()
,负责在堆上分配指定大小的内存。operator new
的实现 类似如下:void* operator new(size_t size) {
if (void* ptr = malloc(size)) // 使用 malloc 分配内存
return ptr;
else
throw std::bad_alloc(); // 分配失败时抛出异常
}
operator new
可以被重载,以自定义内存分配策略。步骤 2:调用构造函数初始化对象:
new
会调用对象的构造函数进行初始化。这是 new
和 malloc
之间最重要的区别之一。malloc
只是分配原始内存,而 new
还会初始化对象。MyClass* obj = new MyClass(5); // 调用构造函数 MyClass(5)
步骤 3:返回正确类型的指针:
new
返回的指针是正确的对象类型的指针,而 malloc
返回 void*
指针,需要进行类型转换。malloc
的底层实现原理malloc
是标准 C 库函数,它只负责在堆上分配指定大小的原始内存,并不会做任何对象的初始化工作。
步骤 1:确定分配大小:
malloc
的输入是分配内存的字节数。它直接向系统申请分配内存块,通常通过调用底层的系统调用(如 brk
或 sbrk
)来调整程序的堆大小,或通过 mmap
分配更大的内存块。void* ptr = malloc(sizeof(MyClass)); // 分配一块足够存储 MyClass 对象的内存
步骤 2:内存分配:
malloc
通过管理程序的堆区域,利用现有的空闲链表、块分配等技术来查找合适的内存块。如果内存块不足,它会向操作系统申请更多内存。malloc
返回指向分配内存的 void*
指针。如果失败,返回 NULL
。步骤 3:无类型的原始内存:
malloc
返回的是无类型的 void*
指针,因此在使用时需要强制类型转换。MyClass* obj = (MyClass*) malloc(sizeof(MyClass));
注意:malloc
只分配内存,不会调用对象的构造函数进行初始化。如果要创建对象并初始化,用户需要手动使用构造函数(比如通过 placement new
)。
delete
和 free()
的区别:
delete
:使用 new
分配的内存需要通过 delete
释放。delete
不仅会释放内存,还会调用对象的析构函数以正确销毁对象,处理资源释放。free()
:malloc
分配的内存需要通过 free()
释放,free()
只会释放内存,不会调用对象的析构函数。new/delete
:
MyClass* obj = new MyClass(10);
delete obj; // 自动调用析构函数
malloc/free
:
int* array = (int*) malloc(10 * sizeof(int));
free(array); // 释放内存
1.1.18 const和define的区别
下面是 const
和 #define
的详细对比表格,包含优缺点、使用场景以及编译处理差异。
const
和 #define
的对比特性 | const |
#define |
---|---|---|
语法 | const 是语言的一部分,属于 C/C++ 关键字 |
#define 是预处理指令,不属于 C/C++ 语言本身 |
类型安全 | 有类型,编译器会进行类型检查 | 没有类型,仅做纯文本替换,不进行类型检查 |
作用域 | 受作用域规则限制,可以是局部或全局 | 没有作用域概念,全局有效,通常在文件范围内生效 |
编译时处理 | 编译时常量,编译器为其分配内存并优化 | 预处理阶段进行文本替换,不分配内存 |
调试支持 | 可以在调试器中查看 const 常量的值 |
由于是文本替换,#define 的值无法在调试器中查看 |
命名空间支持 | 支持命名空间,遵循 C++ 作用域规则 | 不支持命名空间,不遵循作用域规则 |
可变性 | 一旦初始化就无法更改 | 不能更改,因为它只是文本替换 |
内存开销 | 如果是全局 const ,会占用内存,但编译器可能优化 |
不占用内存,但可能会导致代码膨胀 |
复杂表达式 | 可以处理复杂类型和表达式 | 仅能进行简单的文本替换,复杂表达式可能产生问题 |
编译效率 | 编译时会进行类型检查和优化,效率较高 | 由于是预处理替换,编译器不会进行类型检查 |
特性 | const |
#define |
---|---|---|
优点 | - 类型安全,有编译器检查 - 支持调试器查看 - 支持作用域和命名空间 |
- 简单快速 - 无内存占用 - 预处理阶段进行替换 |
缺点 | - 可能有内存开销 - 需要编译器进行更多优化 |
- 无类型安全,容易出错 - 无法调试 - 全局作用域影响大 |
使用场景 | const |
#define |
---|---|---|
常量定义 | 当需要定义类型安全的常量时,例如函数参数、全局配置 | 定义简单的常量值(如常量替换,不关心类型) |
复杂表达式 | 处理复杂类型或表达式,如指针、数组、对象等 | 简单文本替换,无需复杂逻辑 |
调试与优化 | 需要进行调试、类型检查、或优化时 | 不需要类型检查,仅做简单的值替换 |
处理过程 | const |
#define |
---|---|---|
编译过程 | 编译器进行类型检查并为其分配内存,同时进行优化 | 预处理器在编译之前进行文本替换,不做任何类型检查 |
内存管理 | 可能分配内存,但编译器可能会优化为纯常量处理 | 无需分配内存 |
效率影响 | 类型安全,编译器可根据常量进行优化 | 由于是文本替换,效率高,但缺乏类型安全性 |
在C++中,函数指针 和 指针函数 是两个不同的概念。它们在语法和功能上有所区别,下面分别解释它们的含义和区别。
特性 | 函数指针 | 指针函数 |
---|---|---|
定义 | 指向函数的指针,存储的是函数的地址 | 返回一个指针类型的函数,返回值是指针 |
语法 | 返回类型 (*指针名称)(参数类型) |
返回类型 *函数名称(参数类型) |
用途 | 用于动态调用函数、回调机制等 | 用于返回指针,如动态内存分配、引用局部变量等 |
调用方式 | 通过函数指针调用函数 | 通过指针函数的返回值操作指针 |
示例 | void (*funcPtr)(int) |
int* getPointer() |
使用场景 | 回调机制、算法选择等场景 | 动态内存管理、复杂数据结构操作等 |
两者在语法结构上容易混淆,但概念上不同:函数指针是指向函数的指针、其指向一个函数,而指针函数是返回指针的函数,其返回值为指针。
返回类型 (*指针名称)(参数类型)
示例:
#include
void func(int x) {
std::cout << "Value: " << x << std::endl;
}
int main() {
// 定义一个指向函数的指针
void (*funcPtr)(int) = func;
// 通过函数指针调用函数
funcPtr(10); // 输出:Value: 10
return 0;
}
解释:
void (*funcPtr)(int)
表示一个返回类型为 void
,参数为 int
的函数指针。funcPtr(10)
调用函数 func
,效果等同于直接调用 func(10)
。返回类型 *函数名称(参数类型)
示例:
#include
int* getPointer() {
static int value = 42;
return &value;
}
int main() {
// 调用指针函数,获取返回的指针
int* ptr = getPointer();
std::cout << "Value: " << *ptr << std::endl; // 输出:Value: 42
return 0;
}
解释:
int* getPointer()
表示这是一个返回 int
类型指针的函数。getPointer()
返回指向 value
的指针,*ptr
通过该指针访问 value
的值。声明 | 含义 | 特点 |
---|---|---|
const int *a |
a 是一个指向 const int 的指针 |
- 可以修改指针指向 - 不能修改指向的值 |
int const *a |
同 const int *a |
- 可以修改指针指向 - 不能修改指向的值 |
const int a |
a 是一个 const int 常量 |
- 不能修改变量 a 的值 |
int *const a |
a 是一个指向 int 的常量指针 |
- 不能修改指针指向 - 可以修改指向的值 |
const int *const a |
a 是一个指向 const int 的常量指针 |
- 不能修改指针指向 - 不能修改指向的值 |
const
的位置:无论 const
放在类型的前面还是后面,效果是一样的。const int *a
和 int const *a
是等效的,都是指针指向的值不可修改。const
性:当 const
出现在 *
之后时,意味着指针本身是常量,不能改变指针指向的地址;如果出现在类型前,则意味着指针指向的值不可修改。const
和指针,影响了指针本身和指向内容的可变性。const int *a
或 int const *a
a
是一个指向 const int
的指针。指针指向的内容(即 int
类型的值)是常量,不能通过该指针修改内容,但指针本身可以改变。a
使其指向其他地址。示例:
const int *a;
int x = 10;
a = &x; // 可以修改指针,使其指向其他变量
*a = 20; // 错误!不能修改指向的值
const int a
或 int const a
a
是一个 const int
类型的常量。即 a
是一个整型常量,它的值不能被修改。a
是一个常量,不能修改其值。示例:
const int a = 10;
a = 20; // 错误!不能修改常量的值
int *const a
a
是一个指向 int
类型的常量指针。指针本身是常量,不能修改指针的地址,但是可以修改指针指向的值。a
修改其指向的值。a
使其指向其他地址。示例:
int x = 10;
int *const a = &x;
*a = 20; // 可以修改指向的值
a = &y; // 错误!不能修改指针的指向
const int * const a
a
是一个指向 const int
类型的常量指针。即指针本身和指针指向的内容都不能修改。示例:
int x = 10;
const int * const a = &x;
*a = 20; // 错误!不能修改指向的值
a = &y; // 错误!不能修改指针的指向
1.1.21 使用指针需要注意什么?
在使用指针时,需要注意以下几个重要事项,以避免常见错误和潜在的程序崩溃问题:
注意事项 | 问题描述 | 建议 |
---|---|---|
指针初始化 | 未初始化指针可能指向随机内存地址,导致未定义行为 | 在声明指针时,将其初始化为 nullptr |
避免野指针 | 指向已释放或未分配内存的指针,可能导致崩溃或数据损坏 | 释放内存后将指针设置为 nullptr |
指针越界 | 超过数组或动态内存范围,访问无效地址 | 确保访问指针时在合法范围内 |
释放动态分配的内存 | 未释放内存会导致内存泄漏 | 动态分配的内存使用完后应及时释放 |
避免多次释放 | 重复释放同一指针可能导致程序崩溃 | 释放后将指针置为 nullptr |
指针类型匹配 | 指针类型和实际对象类型不匹配,可能导致数据读取错误 | 确保指针类型与实际数据类型一致 |
避免悬空指针 | 指针指向已释放的内存,继续使用该指针会产生错误 | 释放后指针设为 nullptr ,避免访问已释放的内存 |
合理使用指针时,必须特别注意它的生命周期、内存管理和边界条件,避免常见的指针错误,确保程序的稳定性和安全性。
区别点 | 内联函数 | 普通函数 |
---|---|---|
调用方式 | 编译时直接在调用处插入函数体的代码,避免函数调用的开销。 | 通过函数调用机制进行调用,存在参数压栈和返回地址的开销。 |
函数体替换 | 编译器将内联函数的代码在每个调用点直接展开,而不是跳转调用。 | 通过跳转调用,进入函数体执行后再返回调用处。 |
性能影响 | 减少了函数调用的开销(如压栈、跳转等),适合频繁调用的小函数。 | 函数调用有一定的开销,适合较大的、复杂的函数体。 |
代码大小 | 可能导致代码膨胀(code bloat),因为函数体被重复插入多次。 | 代码大小较小,因为函数体只定义一次,调用时通过跳转执行。 |
使用限制 | 不能用于递归函数、包含复杂循环或大量代码的函数。 | 适用于任何函数,包括递归和复杂的函数体。 |
调试 | 不容易调试,因为内联函数可能在编译时被展开,调试信息可能不准确。 | 调试方便,函数调用保留了明确的跳转地址和栈帧信息。 |
内联函数主要用于减少函数调用的开销,尤其是对频繁调用的小型函数。它的作用包括以下几点:
提升性能:通过在调用处直接展开函数体代码,避免了普通函数调用时的参数传递、栈帧保存、跳转和返回等开销。特别适用于频繁调用的小函数。
减少函数调用开销:普通函数调用时,涉及到压栈、跳转、返回等开销。而内联函数直接将函数体插入调用点,避免了这些开销,提高了执行效率。
增强代码可读性:内联函数可以替代宏定义,在保持代码简洁性的同时,内联函数具有类型安全性和语法检查,避免宏带来的隐患。
减少函数调用跳转:内联函数避免了跳转到函数地址执行的过程,尤其在频繁调用的小型函数上,可以显著提升性能。
inline int add(int a, int b) {
return a + b;
}
int main() {
int result = add(3, 4); // 在编译时会直接插入add的函数体代码
}
在这个例子中,add
函数是一个内联函数,编译器可能会将它直接展开到 main
函数的调用处,从而避免函数调用的额外开销。
需要注意的是,内联只是建议,编译器不一定会将所有标记为 inline
的函数展开。
在 C++ 中,主要有三种传值方式:按值传递、按引用传递和按指针传递。它们之间的区别在于函数如何接收和操作参数的数据。以下是每种传递方式的简述及它们之间的区别:
传值方式 | 是否复制数据 | 是否能修改实参 | 效率 | 使用场景 |
---|---|---|---|---|
按值传递 | 是 | 否 | 较低(数据大时) | 小数据类型,只读参数,函数内部不修改外部数据 |
按引用传递 | 否 | 是 | 较高(无复制开销) | 大对象传递,修改外部数据,避免复制大量数据 |
按指针传递 | 否 | 是 | 较高(无复制开销) | 动态内存管理、数组传递,灵活传递 nullptr 指针做检查 |
int
、float
),但是对于大型数据结构(如对象、数组)效率较低,因为需要进行复制操作。void func(int x) {
x = 10; // 不会影响外部变量
}
void func(int &x) {
x = 10; // 会影响外部变量
}
nullptr
进行参数控制。void func(int *x) {
if (x) {
*x = 10; // 修改指针指向的实参
}
}
const
关键字保证函数内部无法修改参数的值。void func(const int &x) {
// x 不能被修改
}
总结来说,选择传递方式时需要权衡数据的大小、是否需要修改实参以及性能要求。在处理较大数据结构时,引用和指针传递通常比按值传递效率更高,而按常量引用传递是保护数据的一种安全选择。
const* 是常量指针; *const是指针常量.
语法 | 指向的值是否可变 | 指针本身是否可变 |
---|---|---|
const int *ptr |
否 | 是 |
int *const ptr |
是 | 否 |
const int *const ptr |
否 | 否 |
const int *ptr
或 int const *ptr
。int *const ptr
。const int *const ptr
。const int *ptr
(或者 int const *ptr
):解释:指向 const
类型的指针。
特点:指针指向的内容不能修改,但指针本身可以改变,即可以让指针指向其他对象。
const int *ptr = &a; // 或者 int const *ptr = &a;
*ptr = 5; // 错误!不能修改指向的值
ptr = &b; // 正确!可以修改指针的指向
总结:这里的 const
修饰的是指针指向的值,表示通过该指针不能修改其指向的对象,但指针本身可以指向其他地方。
int *const ptr
:解释:指向不可变的指针。
特点:指针本身是 const
的,不能指向其他对象,但指针指向的内容可以修改。
int *const ptr = &a;
*ptr = 5; // 正确!可以修改指向的值
ptr = &b; // 错误!不能修改指针的指向
总结:这里的 const
修饰的是指针本身,表示该指针在初始化后不能再指向其他对象,但指针指向的对象可以被修改。
const int *const ptr
:解释:指针本身和指针指向的内容都不可变。
特点:指针既不能指向其他对象,指向的内容也不能被修改。
const int *const ptr = &a;
*ptr = 5; // 错误!不能修改指向的值
ptr = &b; // 错误!不能修改指针的指向
总结:const
修饰了指针和指向的对象,表示指针的值和指向的对象都不能修改。