1. final
和 override
关键字
在 C++ 中,final
和 override
关键字是在面向对象编程中用于处理类的继承和多态的。它们主要用于管理派生类和虚函数,提供额外的安全性和代码可读性,防止意外的函数重写或错误的重载行为。
1. final
关键字
final
关键字用于防止进一步的继承或函数重写。它可以应用于类或虚函数。
1.1 用在类上
当 final
用在类上时,表示该类不能被继承,禁止其他类从这个类派生。
class Base final {
// 类内容
};
// 错误:不能从一个被标记为 final 的类继承
class Derived : public Base {
};
1.2 用在虚函数上
当 final
用在虚函数上时,表示该虚函数在派生类中不能被重写。即使派生类继承了这个类,也不能对该虚函数进行重写。
class Base {
public:
virtual void show() final {
std::cout << "Base class" << std::endl;
}
};
class Derived : public Base {
public:
// 错误:不能重写被标记为 final 的函数
void show() override {
std::cout << "Derived class" << std::endl;
}
};
2. override
关键字
override
关键字用在派生类的虚函数声明中,表示该函数是要重写基类中的虚函数。它让编译器帮助检查你是否正确地重写了基类的虚函数,从而避免因函数签名不匹配而导致意外的行为。
2.1 使用 override
的好处
- 防止错误的函数重写:如果函数签名与基类虚函数不匹配,编译器会报错,从而避免隐藏 bug。
- 提高代码可读性:明确表达这个函数是对基类虚函数的重写。
class Base {
public:
virtual void show() {
std::cout << "Base class" << std::endl;
}
};
class Derived : public Base {
public:
void show() override { // 明确表示这是重写基类的 show 函数
std::cout << "Derived class" << std::endl;
}
};
如果派生类函数没有正确匹配基类的虚函数(例如参数或返回值不同),使用 override
会导致编译错误:
class Base {
public:
virtual void show(int x) {
std::cout << "Base class" << std::endl;
}
};
class Derived : public Base {
public:
void show() override { // 错误:没有匹配到基类的虚函数
std::cout << "Derived class" << std::endl;
}
};
3. final
和 override
的组合使用
你可以同时使用 final
和 override
,以表示一个派生类中的函数是重写基类函数的,并且不允许在更进一步的派生类中重写该函数。
class Base {
public:
virtual void show() {
std::cout << "Base class" << std::endl;
}
};
class Derived : public Base {
public:
void show() override final { // 重写基类的虚函数,且不可再被重写
std::cout << "Derived class" << std::endl;
}
};
// 错误:不能重写被标记为 final 的函数
class FurtherDerived : public Derived {
public:
void show() override {
std::cout << "FurtherDerived class" << std::endl;
}
};
总结
-
final
:- 用于禁止类的继承或禁止虚函数的进一步重写。
- 用在类上,防止类被继承。
- 用在虚函数上,防止该函数在派生类中被重写。
-
override
:- 用于明确标记派生类中的虚函数是对基类虚函数的重写。
- 帮助编译器进行类型检查,确保正确重写基类的虚函数。
- 提高代码的可读性,减少潜在的函数重写错误。
2.宏定义和函数的区别
特性 | 宏定义(#define ) | 函数 |
---|---|---|
处理方式 | 预处理器的文本替换 | 编译器生成实际的机器代码 |
类型检查 | 无类型检查,纯文本替换 | 有严格的类型检查 |
执行效率 | 没有函数调用开销,但有重复计算风险 | 有调用开销,但支持内联函数 |
调试难度 | 难以调试,宏替换后不生成调试信息 | 易于调试,有明确的调用栈和调试信息 |
作用域 | 全局作用域,没有局部作用域控制 | 遵循 C++ 作用域规则 |
表达能力 | 灵活,能处理文本替换、常量、表达式 | 更灵活,支持复杂逻辑、控制流、递归等 |
维护性 | 难以维护,容易引入隐藏错误 | 容易维护,逻辑清晰可复用 |
- 宏定义通过文本替换实现,灵活但缺乏类型检查,容易引入隐蔽错误,在复杂项目中应慎用。
- 函数提供更安全的类型检查、更灵活的逻辑控制,易于调试和维护,是推荐的方式,尤其是当逻辑复杂时。
3.sizeof
和 strlen
的区别
在 C++ 中,sizeof
和 strlen
是两个用于不同目的的操作符/函数。sizeof
是一个编译时操作符,返回对象或类型的字节大小;而 strlen
是一个函数,用于计算字符串的长度,即字符串中字符的个数(不包括终止字符 \0
)。
1. 功能
-
sizeof
:- 用于计算数据类型或对象的大小,返回对象在内存中占用的字节数。
sizeof
是一个编译时操作符,它的计算在编译时完成。- 适用于所有类型,包括内置类型、数组、结构体、类等。
int arr[10]; std::cout << "Size of int: " << sizeof(int) << " bytes" << std::endl; // 通常 4 bytes std::cout << "Size of array: " << sizeof(arr) << " bytes" << std::endl; // 通常 40 bytes (10 * 4)
-
strlen
:- 用于计算 C 风格字符串的长度,即字符数组中有效字符的个数,不包括字符串末尾的空字符
\0
。 strlen
是一个运行时函数,需要遍历字符串才能计算长度,因此它在运行时计算字符串长度。
const char* str = "Hello"; std::cout << "Length of string: " << strlen(str) << " characters" << std::endl; // 输出 5
- 用于计算 C 风格字符串的长度,即字符数组中有效字符的个数,不包括字符串末尾的空字符
2. 工作原理
-
sizeof
:- 直接返回变量或类型在内存中占用的总字节数,对于数组来说,
sizeof
返回的是整个数组的大小(即元素个数乘以单个元素的大小)。
- 直接返回变量或类型在内存中占用的总字节数,对于数组来说,
-
strlen
:- 遍历字符串,找到第一个空字符
\0
之前的字符数。返回的是字符串中不包括空终止符的字符个数。
- 遍历字符串,找到第一个空字符
3. 适用对象
-
sizeof
:- 适用于任何数据类型:基本类型(如
int
、double
)、指针、数组、结构体、类等。 - 对于数组,
sizeof
返回数组的总大小,而不是元素个数。 - 对于指针,
sizeof
返回的是指针自身的大小(通常是 4 或 8 字节,取决于系统架构),而不是它指向的数据的大小。
- 适用于任何数据类型:基本类型(如
-
strlen
:- 仅适用于C 风格的字符串(即以
\0
结尾的字符数组),它不能用于其他数据类型。 strlen
不能直接用于非字符串的数组或对象。
- 仅适用于C 风格的字符串(即以
4. 计算数组大小
-
sizeof
可以用于计算数组的大小: -
strlen
不能用于计算非字符串数组的大小,只能用于 C 字符串:
5. 返回值类型
-
sizeof
:- 返回值类型为
size_t
,它是无符号整数类型,表示内存中的字节数。
- 返回值类型为
-
strlen
:- 也返回
size_t
类型,但表示的是字符串中的字符个数,不包括空终止符。
- 也返回
6. 编译时 vs 运行时
-
sizeof
:- 是编译时操作,编译器在编译阶段就计算出对象的大小,不会在运行时动态计算。
-
strlen
:- 是运行时函数,它在程序运行时遍历字符串计算长度。
特性 | sizeof | strlen |
---|---|---|
功能 | 返回对象或类型的内存大小 | 返回 C 字符串的长度(不包括终止符) |
工作方式 | 编译时操作,计算字节数 | 运行时函数,遍历字符串计算字符个数 |
适用对象 | 适用于任何类型(包括数组、结构体、指针等) | 仅适用于 C 风格字符串(以 \0 结尾的字符数组) |
返回值 | 对象的内存大小(以字节为单位) | 字符串的长度(不包括 \0 ) |
编译时/运行时 | 编译时计算 | 运行时计算 |
作用对象 | 任意类型 | 仅限字符数组(C 字符串) |
sizeof
用于计算任意类型的大小,适用于数组、指针、结构体等所有类型,结果是在内存中占用的字节数。strlen
用于计算 C 风格字符串的长度,只能用于以\0
结尾的字符数组,返回有效字符的个数,不包括终止符。
对于处理内存大小或对象大小,应该使用 sizeof
;而对于计算字符串的长度,应该使用 strlen
。
4.strcpy
、sprintf
和 memcpy
的区别
在 C 和 C++ 中,strcpy
、sprintf
和 memcpy
都是常用的函数,它们主要用于复制数据,但它们的具体用途和工作方式有所不同。
1. 功能
-
strcpy
:- 用于将一个C 风格字符串(以
\0
结尾)复制到另一个字符串。 - 复制时会一直拷贝直到遇到字符串的终止符
\0
。 - 适用于字符串的拷贝,源字符串必须是以
\0
结尾的有效字符串。
char src[] = "Hello"; char dest[10]; strcpy(dest, src); // 将 "Hello" 复制到 dest
- 用于将一个C 风格字符串(以
-
sprintf
:- 类似于
printf
,但它将格式化的输出写入目标字符串而不是输出到控制台。 - 支持格式化功能(如
%d
、%f
、%s
等),因此它常用于将不同类型的数据格式化成字符串。
int num = 42; char buffer[50]; sprintf(buffer, "Number is: %d", num); // 将 "Number is: 42" 格式化并写入 buffer
- 类似于
-
memcpy
:- 用于从一个内存地址将任意类型的数据块复制到另一个内存地址。
- 它不关心数据类型或内容,只按指定的字节数进行内存块的直接拷贝,因此适用于任何类型的数据(例如数组、结构体等)。
- 不会在数据末尾添加
\0
,也不会处理字符串结束标记。
int src[] = {1, 2, 3, 4}; int dest[4]; memcpy(dest, src, sizeof(src)); // 直接复制 16 字节的内存(4 个整数)
2. 工作原理
-
strcpy
:- 它逐个字符复制源字符串到目标字符串,直到遇到字符串的终止符
\0
,并将该终止符也复制到目标字符串中。 - 源和目标必须是以
\0
结尾的字符数组(C 风格字符串)。
- 它逐个字符复制源字符串到目标字符串,直到遇到字符串的终止符
-
sprintf
:- 将格式化后的数据写入目标字符串。例如,
%d
表示整数,%s
表示字符串。 - 比
strcpy
更灵活,可以将整数、浮点数等格式化为字符串并输出。
- 将格式化后的数据写入目标字符串。例如,
-
memcpy
:- 按指定的字节数直接复制内存中的数据,无论数据类型是什么。它不检查内容,因此适用于二进制数据的复制。
- 例如,复制数组、结构体等。
3. 适用范围
-
strcpy
:- 仅适用于 C 风格字符串(即以
\0
结尾的字符数组),它不能用于非字符串的数据类型。
- 仅适用于 C 风格字符串(即以
-
sprintf
:- 适用于将数据格式化为字符串,包括多种数据类型,如整数、浮点数、字符串等。
- 常用于生成带有格式的输出字符串。
-
memcpy
:- 适用于任何类型的数据,包括字符串、数组、结构体等。它用于直接复制内存,而不做任何数据格式的转换或检查。
4. 安全性
-
strcpy
:- 容易发生缓冲区溢出,因为它不会检查目标缓冲区的大小,可能会导致内存安全问题。
- 应该使用
strncpy
进行更安全的字符串拷贝,限制最大拷贝长度。
char src[] = "Hello"; char dest[4]; // 太小,可能溢出 strcpy(dest, src); // 危险!
strncpy(dest, src, sizeof(dest) - 1); // 复制指定长度,确保不溢出 dest[sizeof(dest) - 1] = '\0'; // 手动添加 '\0'
-
sprintf
:- 也有缓冲区溢出风险,因为它不检查目标缓冲区的大小。
- 可以使用
snprintf
,提供缓冲区长度限制。
char buffer[10]; sprintf(buffer, "Value: %d", 123456); // 可能导致溢出
snprintf(buffer, sizeof(buffer), "Value: %d", 123456); // 安全
-
memcpy
:- 需要开发者保证源和目标内存的大小和类型正确。如果源和目标重叠,则应该使用
memmove
,避免数据损坏。 - 直接操作内存块,没有内容的类型检查。
- 需要开发者保证源和目标内存的大小和类型正确。如果源和目标重叠,则应该使用
5. 是否处理终止符
-
strcpy
:- 会复制字符串的终止符
\0
,确保目标字符串也是以\0
结尾。
- 会复制字符串的终止符
-
sprintf
:- 格式化字符串后会自动添加终止符
\0
。
- 格式化字符串后会自动添加终止符
-
memcpy
:- 不会自动处理或添加终止符。它只是按指定字节数直接复制内存,因此不会关心字符串的结束符
\0
。
- 不会自动处理或添加终止符。它只是按指定字节数直接复制内存,因此不会关心字符串的结束符
6. 性能
-
strcpy
:- 逐字符复制,性能较好,但只适用于字符串。
-
sprintf
:- 由于涉及到格式化操作,性能可能会较
strcpy
和memcpy
慢,尤其是在处理复杂格式时。
- 由于涉及到格式化操作,性能可能会较
-
memcpy
:- 性能通常优于
strcpy
和sprintf
,因为它是基于字节的内存复制,没有类型检查和格式化过程。
- 性能通常优于
特性 | strcpy | sprintf | memcpy |
---|---|---|---|
功能 | 复制 C 字符串,包含 \0 | 将格式化数据写入字符串 | 直接复制内存数据,无类型检查 |
适用对象 | 仅适用于 C 字符串 | 适用于字符串和多种数据类型的格式化 | 适用于任意数据类型,直接复制内存 |
终止符处理 | 自动复制字符串终止符 \0 | 自动添加终止符 \0 | 不处理终止符,按指定字节数复制 |
安全性 | 容易缓冲区溢出,推荐使用 strncpy | 容易缓冲区溢出,推荐使用 snprintf | 需要确保内存大小正确,推荐使用 memmove 避免重叠 |
性能 | 适中,适用于字符串复制 | 较慢,尤其是处理复杂格式化时 | 高效,直接内存拷贝 |
strcpy
:用于 C 风格字符串的复制,会处理字符串的终止符\0
,但有缓冲区溢出风险,应使用strncpy
更安全。sprintf
:用于格式化数据并写入字符串,适用于处理多种类型的数据,但性能较低,存在溢出风险,推荐使用snprintf
。memcpy
:用于任意类型的数据块复制,按字节数直接复制内存,不会处理字符串终止符,适用于高效的内存操作。
5.结构体可以直接赋值吗
在 C 和 C++ 中,结构体是可以直接赋值的,不过它们的行为可能有所不同,具体看是否满足条件。
1. C 语言中的结构体赋值
在 C 语言中,结构体支持直接赋值操作,从一个结构体变量复制内容到另一个结构体变量。这是通过按位复制(bitwise copy)的方式完成的,也叫浅拷贝。
#include <stdio.h>
struct Point {
int x;
int y;
};
int main() {
struct Point p1 = {10, 20};
struct Point p2;
// 结构体直接赋值
p2 = p1;
printf("p2.x = %d, p2.y = %d\n", p2.x, p2.y); // 输出: p2.x = 10, p2.y = 20
return 0;
}
在上面的代码中,p2 = p1
是一个合法的操作,它会将 p1
的内容按位复制给 p2
,因此 p2
的成员将和 p1
相同。
2. C++ 语言中的结构体赋值
C++ 是 C 的超集,所以 C++ 也支持结构体的直接赋值,和 C 一样是通过浅拷贝实现的。但在 C++ 中,如果结构体包含了指针或动态分配的资源,浅拷贝可能导致资源共享问题或内存泄漏,这时就需要重载赋值运算符来进行深拷贝操作。
#include <iostream>
struct Point {
int x;
int y;
};
int main() {
Point p1 = {10, 20};
Point p2;
// 结构体直接赋值
p2 = p1;
std::cout << "p2.x = " << p2.x << ", p2.y = " << p2.y << std::endl; // 输出: p2.x = 10, p2.y = 20
return 0;
}
和 C 一样,在 C++ 中也是合法的,结构体中的所有成员都会被复制。
3. 结构体中包含指针的情况
如果结构体包含指针成员,直接赋值时只是将指针地址复制,而不是指向的数据内容。此时两者将共享同一块内存,修改其中一个结构体的指针内容会影响到另一个。这在某些情况下会带来潜在的风险,特别是当涉及动态内存分配时。
#include <iostream>
#include <cstring> // 用于 strcpy
struct Person {
char* name;
};
int main() {
Person p1;
p1.name = new char[20];
strcpy(p1.name, "Alice");
Person p2;
p2 = p1; // 直接赋值,浅拷贝
std::cout << "p2.name: " << p2.name << std::endl; // 输出 "Alice"
// 修改 p2.name 将影响 p1.name
strcpy(p2.name, "Bob");
std::cout << "p1.name: " << p1.name << std::endl; // 输出 "Bob",p1.name 被修改
// 注意需要正确释放内存
delete[] p1.name; // p1.name 和 p2.name 指向同一块内存,释放一次即可
return 0;
}
解决方法:
为了避免指针共享问题,可以在结构体中实现深拷贝,通过重载赋值运算符或使用智能指针来管理动态内存。
#include <iostream>
#include <cstring>
struct Person {
char* name;
// 自定义赋值运算符,实现深拷贝
Person& operator=(const Person& other) {
if (this == &other) return *this; // 防止自我赋值
delete[] name; // 释放已有内存
name = new char[strlen(other.name) + 1]; // 分配新内存
strcpy(name, other.name); // 复制内容
return *this;
}
// 析构函数,防止内存泄漏
~Person() {
delete[] name;
}
};
int main() {
Person p1;
p1.name = new char[20];
strcpy(p1.name, "Alice");
Person p2;
p2 = p1; // 深拷贝
std::cout << "p2.name: " << p2.name << std::endl; // 输出 "Alice"
// 修改 p2.name 不会影响 p1.name
strcpy(p2.name, "Bob");
std::cout << "p1.name: " << p1.name << std::endl; // 仍然输出 "Alice"
return 0;
}
总结:
- C 和 C++ 都支持结构体的直接赋值,复制时进行浅拷贝(bitwise copy),即成员逐位复制。
- 浅拷贝的风险:如果结构体中包含指针或动态分配的资源,浅拷贝可能导致内存泄漏或资源共享问题。
- 在 C++ 中,可以通过重载赋值运算符实现深拷贝,确保数据正确复制,而不是仅复制指针地址。
6.volatile
关键字的作用
在 C 和 C++ 中,volatile
是一个类型修饰符,告诉编译器不要对被修饰的变量进行优化。这意味着编译器在每次访问这个变量时,必须直接从内存读取或写入该变量,而不是使用寄存器或缓存中的值。这在某些情况下(如硬件寄存器、信号处理、并发编程等)非常重要。
主要作用:
-
防止编译器优化:当一个变量可能会被系统之外的机制修改(例如由硬件设备或其他线程),编译器通常无法意识到这种变化。如果不使用
volatile
,编译器可能会对其进行优化,比如将其值保存在寄存器中,从而导致程序读取的值并不是最新的。volatile
关键字确保每次对变量的读取或写入操作都直接发生在内存中。 -
确保变量的最新值:它确保程序在每次访问
volatile
变量时,都能得到该变量的最新值,即使这个变量可能是被外部机制改变的。
典型的使用场景:
-
硬件寄存器访问:例如嵌入式编程中,直接访问某些硬件寄存器时,寄存器的值可能会被硬件修改,使用
volatile
可以确保程序每次都读取硬件寄存器的最新值。 -
中断处理程序:当中断处理程序修改某些变量时,主程序中的变量声明为
volatile
,确保主程序总是能读取到被中断修改后的最新值。 -
多线程环境:当一个变量可能被多个线程访问时,
volatile
可以防止编译器对变量的访问进行不安全的优化。然而,volatile
本身并不能解决多线程中的同步问题(比如并发访问导致的竞态条件),这通常需要用到其他同步机制(如mutex
、atomic
等)。
1. 硬件寄存器访问
volatile int* status_register = (volatile int*) 0xFFFF0000; // 假设这是硬件寄存器的地址
while (*status_register == 0) {
// 等待硬件状态改变
}
这里,status_register
是一个指向硬件寄存器的指针,使用 volatile
确保程序每次都读取该寄存器的最新值,而不是使用缓存的旧值。
2. 中断处理中的使用
volatile int interrupt_flag = 0;
void interrupt_handler() {
interrupt_flag = 1; // 中断处理程序修改这个变量
}
int main() {
while (interrupt_flag == 0) {
// 主程序等待中断发生
}
// 中断发生后执行的代码
return 0;
}
在这个例子中,interrupt_flag
由中断处理程序修改。使用 volatile
关键字可以确保主程序总是能读取到最新的 interrupt_flag
值。
volatile
的局限性:
-
不提供原子性:
volatile
仅保证对变量的直接访问,但不保证对变量的操作是原子的。如果需要确保多线程之间的同步,还需要使用其他机制,如互斥锁(mutex
)或原子操作(std::atomic
)。 -
不适合复杂的内存模型:在现代多线程编程中,内存模型和缓存一致性非常复杂,
volatile
并不能确保内存屏障的作用。因此,在线程同步中,推荐使用更高级的工具(如原子操作和同步原语)。
总结:
volatile
关键字的主要作用是防止编译器优化对变量的访问,确保每次访问都直接从内存中获取或写入。- 典型场景包括硬件寄存器访问、信号处理程序或中断处理、多线程环境中的共享变量。
- 注意:
volatile
并不能保证变量的线程安全性,需要与其他同步机制结合使用。
7.全局变量和局部变量有什么区别?操作系统和编译器是怎么知道的?
在 C++ 和其他编程语言中,全局变量和局部变量有以下几个关键区别:
特性 | 全局变量 | 局部变量 |
---|---|---|
作用域 | 作用域为整个程序文件,所有函数或代码块都能访问。 | 作用域仅限于声明它的函数或代码块。 |
生命周期 | 从程序开始执行到结束一直存在。 | 只在其所在的函数或代码块执行时存在,函数返回后销毁。 |
存储位置 | 通常存储在数据段或 BSS 段。 | 通常存储在栈(stack)中,或寄存器中。 |
初始值 | 如果未显式初始化,默认初始化为零值(0, NULL等)。 | 如果未初始化,其值是未定义的(栈中残留值)。 |
访问权限 | 可以通过函数或代码块在任意地方访问。 | 只能在声明它的函数或代码块内访问。 |
存储空间 | 占用静态存储空间(静态分配)。 | 占用动态存储空间(通常为栈)。 |
操作系统和编译器是如何区分的?
操作系统本身并不直接关心变量的作用域和生命周期。这些问题是在编译器的层面处理的。编译器通过变量的声明位置和存储类型来区分全局变量和局部变量,并生成相应的代码。
1. 编译器区分全局变量和局部变量:
-
全局变量:编译器会根据变量声明的位置来判断一个变量是否是全局变量。如果变量在所有函数外部声明,它就被认为是全局变量。全局变量在编译时被存储在程序的数据段(
.data
段,已初始化的全局变量)或 BSS 段(.bss
段,未初始化的全局变量)。编译器会为全局变量分配内存,并且在整个程序生命周期中维护该变量的值。 -
局部变量:局部变量是在函数或代码块内声明的变量,编译器将这些变量分配到栈(stack)中。函数或代码块执行时,栈会自动分配局部变量的存储空间,函数返回时局部变量的空间会被释放。
2. 内存布局:
典型的程序内存布局(主要关注静态和动态分配的存储区域)如下:
- 代码段:存放程序的机器指令。
- 数据段(.data 段):存放已初始化的全局变量。
- BSS 段(.bss 段):存放未初始化的全局变量。
- 堆:存储动态分配的内存(如通过
malloc
或new
分配)。 - 栈:存储局部变量和函数调用信息。
3. 编译器如何知道变量的类型:
-
符号表(Symbol Table):编译器在编译时会为每个变量创建符号表,记录变量的名字、类型、作用域、存储位置等信息。对于全局变量,编译器会将它标记为程序开始到结束都有效的变量;而局部变量只在其作用域内有效。
-
作用域规则:编译器通过作用域规则(Scope Rules)来决定何时创建和销毁变量。全局变量的作用域是整个程序,而局部变量的作用域是其声明所在的函数或代码块。
4. 链接器的工作:
在编译和链接过程中,链接器(Linker)会负责将所有全局变量放在程序的全局内存区段,确保它们在不同的模块和文件间共享。而局部变量不需要链接器的特别处理,因为它们只存在于栈中,作用域限制在局部。
操作系统的参与:
操作系统并不直接管理全局变量和局部变量的存储。它主要通过为程序分配内存(如栈空间、堆空间、全局内存段)来支持程序运行。具体的变量管理是编译器和运行时环境的责任。操作系统只需要提供一个合适的内存模型,并在程序运行时分配和管理栈、堆、代码段等内存区域。
-
栈管理:操作系统通过分配栈空间来支持局部变量的生命周期管理。每个线程有自己的栈,栈的大小通常在操作系统的配置中指定。局部变量随函数调用进栈,在函数返回时出栈。
-
全局变量的管理:全局变量的存储区域由操作系统的内存管理模块分配。程序加载时,操作系统会将数据段和 BSS 段映射到进程的虚拟内存空间。
总结:
- 全局变量的作用域是整个程序,生命周期从程序开始到结束,存储在数据段或 BSS 段。
- 局部变量的作用域仅限于声明它的函数或代码块,生命周期仅限于函数的执行期间,存储在栈中。
- 编译器通过变量的声明位置和作用域规则来区分全局变量和局部变量,负责为它们分配内存并生成合适的访问代码。
- 操作系统不直接管理变量,而是为程序提供必要的内存模型,分配栈和堆等存储空间,并管理全局内存区段。
8.C++ 中的指针和引用
指针和引用是 C++ 中两种用于间接访问对象或数据的机制,它们都允许程序员通过某个变量的地址来操作该变量的值。尽管功能相似,它们在语法、行为和使用场景上有显著区别。
1. 指针(Pointer)
指针是一个变量,其值是一个内存地址,指向另一个变量或对象的内存位置。指针的主要特点是可以通过解引用(*
操作符)访问该地址处存储的值。
指针的特性:
- 可以指向任意类型的变量。
- 可以通过指针的值(地址)进行运算(如指针加法)。
- 可以被重新赋值,指向不同的地址。
- 可以为空(
nullptr
或NULL
),表示不指向任何有效的对象。 - 可以动态分配和释放内存(如使用
new
和delete
)。
2. 引用(Reference)
引用是某个变量的别名,一旦定义,引用就绑定到特定的对象,不能更改引用指向的对象。引用在定义时必须进行初始化,并且不能为 nullptr
。
引用的特性:
- 必须在声明时初始化,且初始化后不能改变引用的绑定对象。
- 无法进行指针运算(如加减法)。
- 不可以为空(不能有空引用的概念)。
- 语法上更简洁(使用起来像变量本身)。
- 常用于函数参数和返回值的传递,避免值拷贝,提高性能。
指针和引用的区别
特性 | 指针(Pointer) | 引用(Reference) |
---|---|---|
定义 | 是一个变量,保存另一个变量的地址。 | 是一个别名,引用某个已存在的变量。 |
初始化 | 可以声明时不初始化,初始化后也可以修改。 | 必须在声明时初始化,初始化后不能改变所引用的对象。 |
指向空值 | 可以指向空值(nullptr 或 NULL )。 | 不可以引用空值,必须引用一个有效的对象。 |
是否可以重新赋值 | 可以重新指向另一个对象或内存地址。 | 不可以改变引用的对象,一旦绑定不能修改。 |
语法复杂度 | 需要使用 * 和 & 运算符来解引用和取地址。 | 更简洁,使用时像普通变量一样。 |
指针运算 | 可以进行指针运算(如指针加减)。 | 不支持指针运算。 |
内存使用 | 占用额外的内存(存储地址值)。 | 不占用额外的内存,是原始变量的别名。 |
动态内存分配 | 可以指向动态分配的内存(new /delete )。 | 不能直接引用动态分配的内存(除非通过指针)。 |
可用于数组 | 指针可以指向数组,进行遍历或计算。 | 引用不能直接遍历数组。 |
指针传参:
void increment(int* ptr) {
(*ptr)++; // 修改指针所指对象的值
}
int main() {
int a = 10;
increment(&a); // 传递 a 的地址
std::cout << a << std::endl; // 输出 11
return 0;
}
引用传参:
void increment(int& ref) {
ref++; // 直接修改引用的对象
}
int main() {
int a = 10;
increment(a); // 传递 a 的引用
std::cout << a << std::endl; // 输出 11
return 0;
}
在这两个例子中,指针和引用都用于修改函数外部的变量 a
的值。引用使用起来更简洁,不需要使用 &
和 *
来传递和解引用。
总结
-
指针 是存储另一个变量地址的变量,具有更大的灵活性,可以指向不同的对象或空值,支持指针运算,适合需要动态分配内存或处理数组的场景。
-
引用 是现有变量的别名,语法更简洁,更安全(避免空引用和指针运算),但一旦绑定到某个对象就不能再更改,适合参数传递和返回值的优化场景。
9.数组名和指向数组首元素的指针的区别
在 C++ 中,数组名和指向数组首元素的指针看似相似,但它们在概念上和行为上有一些关键的区别。
1. 数组名
- 数组名是一个常量,表示数组首元素的地址,但它本身并不是一个指针类型。
- 数组名的值(地址)不可修改,不能像普通指针一样指向其他地址。
- 数组名可以用来访问整个数组元素,使用数组下标运算符
[]
。
2. 指向数组首元素的指针
- 指针是一个变量,保存某个内存地址,可以指向数组的首元素或其他元素。
- 指针的值(地址)是可修改的,可以重新赋值指向其他内存地址。
- 指针可以通过加法、减法等进行运算(指针算术),遍历数组。
主要区别总结:
特性 | 数组名 | 指向数组首元素的指针 |
---|---|---|
类型 | 数组类型,常量 | 指针类型,变量 |
是否可修改 | 不可修改(是常量),总是指向数组首元素 | 可修改,指针可以指向其他地址 |
指针运算 | 不支持指针运算 | 支持指针运算,如加减运算 |
内存占用 | 直接是数组本身的一部分 | 是独立的变量,占用额外的内存 |
作用 | 表示整个数组 | 表示某个元素的地址 |
int arr[5] = {1, 2, 3, 4, 5};
int* ptr = arr; // ptr 指向数组的首元素,即 arr[0]
// 数组名和指针的基本用法
std::cout << arr[0] << std::endl; // 输出 1,直接使用数组名
std::cout << *ptr << std::endl; // 输出 1,使用指针解引用访问首元素
// 修改指针指向
ptr = &arr[2]; // 现在指向 arr[2]
std::cout << *ptr << std::endl; // 输出 3
// 数组名不可修改
// arr = &arr[1]; // 错误,数组名不能作为左值
10. 一个指针占用多少字节?
指针占用的字节数取决于目标平台的地址位宽。常见的系统位宽有 32 位和 64 位:
-
32 位系统:指针占用 4 字节(32 位),因为地址总线是 32 位,意味着最多可以寻址 2^32 个内存位置(4 GB 内存)。
-
64 位系统:指针占用 8 字节(64 位),因为地址总线是 64 位,意味着最多可以寻址 2^64 个内存位置(理论上可达 16 EB 内存)。
平台示例:
- 在32 位 Windows/Linux 平台上,指针通常占用 4 字节。
- 在64 位 Windows/Linux 平台上,指针通常占用 8 字节。
(验证指针的大小):
#include <iostream>
int main() {
int* ptr = nullptr;
std::cout << "Size of pointer: " << sizeof(ptr) << " bytes" << std::endl;
return 0;
}
运行结果:
- 在 32 位系统上,输出:
Size of pointer: 4 bytes
- 在 64 位系统上,输出:
Size of pointer: 8 bytes
因此,指针占用的字节数取决于系统的架构,通常是 4 或 8 字节。
标签:02,面试题,变量,区别,int,函数,内存,字符串,指针 From: https://blog.csdn.net/qq_50373827/article/details/143055662