文章目录
1. extern关键字
extern
是一个关键字,用于C和C++编程语言中,表示一个变量或函数是在其他文件中定义的,也就是说,它们的定义在其他地方,不在当前文件中。这个关键字对于支持大型项目和模块化编程非常重要,因为它允许程序员在不同的源文件中使用相同的变量或函数。
1.1 原理
extern
关键字告诉编译器,变量或函数的定义可能在别的源文件中。这样,编译器在编译时,就会去其他文件中找这个变量或函数的定义。如果找不到,编译就会失败。
1.2 作用
-
在多个源文件中共享变量: 如果你有一个变量,想在多个源文件中使用,就可以用
extern
关键字。首先,你在一个文件中定义这个变量,然后在其他文件中用extern
关键字声明它。这样,其他文件就可以使用这个变量了。 -
在多个源文件中共享函数: 如果你有一个函数,想在多个源文件中使用,也可以用
extern
关键字。首先,你在一个文件中定义这个函数,然后在其他文件中用extern
关键字声明它。这样,其他文件就可以调用这个函数了。
1.3 使用场景
-
都是C/C++代码
- 在多个源文件中共享变量:这是最常见的使用场景。例如,你可能有一个全局变量,需要在多个文件中使用。你可以在一个文件中定义这个变量,然后在其他文件中使用
extern
关键字声明它。 - 在多个源文件中共享函数:这也是一个常见的使用场景。例如,你可能有一个函数,需要在多个文件中调用。你可以在一个文件中定义这个函数,然后在其他文件中使用
extern
关键字声明它。
- 在多个源文件中共享变量:这是最常见的使用场景。例如,你可能有一个全局变量,需要在多个文件中使用。你可以在一个文件中定义这个变量,然后在其他文件中使用
-
在C++中调用C的代码
- 解决名字修饰(Name Mangling):C++对函数名进行名字修饰(Name Mangling)以支持函数重载,而C不这样做。这可能会导致在C++代码中调用C函数时出现问题,因为C++编译器可能无法识别C函数的名字。
extern "C"
可以告诉C++编译器,这个函数是用C语言写的,不应该对它的名字进行修饰。 - 链接C库:如果你想在C++代码中链接C语言写的库,你需要使用
extern "C"
。这是因为C++编译器需要知道这些函数是用C语言写的,不应该对它们的名字进行修饰。
- 解决名字修饰(Name Mangling):C++对函数名进行名字修饰(Name Mangling)以支持函数重载,而C不这样做。这可能会导致在C++代码中调用C函数时出现问题,因为C++编译器可能无法识别C函数的名字。
1.4 例子
例1:在多个文件中共享变量
假设我们有两个文件,一个是 main.cpp
,一个是 another_file.cpp
。我们想在这两个文件中共享一个变量 shared_variable
。我们可以这样做:
在 another_file.cpp
中:
int shared_variable = 10; // 定义并初始化 shared_variable
在 main.cpp
中:
extern int shared_variable; // 声明 shared_variable
int main() {
std::cout << "Shared Variable: " << shared_variable << std::endl; // 打印 shared_variable 的值
return 0;
}
例2:在 C++ 中调用 C 函数
假设我们有一个 C 函数 c_function
,我们想在 C++ 代码中调用它。我们可以这样做:
在 c_functions.c
文件中:
#include <stdio.h>
void c_function() {
printf("Hello from C function!\n");
}
在 main.cpp
文件中:
extern "C" {
void c_function(); // 声明 C 函数
}
int main() {
c_function(); // 调用 C 函数
return 0;
}
注意:尽管extern
关键字可以使变量和函数在多个源文件中可用,但是过度使用它可能会导致代码的可维护性降低。因为,当你在一个文件中看到一个extern
变量或函数时,你必须去其他文件中找到它的定义,这可能会使代码难以理解和维护。因此,一般建议在必要的时候才使用extern
关键字,并尽量减少全局变量的使用。
2. malloc、free与new、delete的区别
2.1 定义
- malloc、free是C中的库函数
- new、delete是C++中的操作符
- new的流程
- 计算所需内存大小:编译器首先需要知道要分配多少内存。对于基本数据类型(如int、double等),这很简单,因为它们的大小是预先知道的。对于类对象,编译器将计算类的大小,包括其所有非静态成员变量。
- 申请内存:编译器会调用一个函数(通常是
operator new
或者其它类似的库函数),这个函数会向系统的内存管理器请求分配所需的内存。如果内存管理器无法分配足够的内存,那么operator new
会抛出一个std::bad_alloc
异常(除非你使用的是new(std::nothrow)
,在这种情况下,operator new
会返回一个nullptr
)。 - 构造对象:如果请求的内存成功分配,编译器接下来会在分配的内存上构造对象。这包括调用对象的构造函数(如果有的话)。如果对象的构造函数抛出异常,那么已经分配的内存将被释放,然后异常将被传播给
new
运算符的调用者。 - 返回指针:最后,
new
运算符返回一个指向新创建的对象的指针。
- delete的流程
- 调用析构函数:对于类类型的对象,编译器首先调用对象的析构函数。这是为了执行任何必要的清理工作,例如释放对象可能拥有的其他资源(如动态分配的内存,打开的文件句柄等)。对于基本类型(如
int
,double
等),没有析构函数要调用。 - 释放内存:析构函数调用完成后,编译器会调用一个函数(通常是
operator delete
或其他类似的库函数)来释放对象占用的内存。这个函数告诉内存管理器这块内存现在可以被重新分配给其他对象。 - 需要注意的是,
delete
仅仅释放内存,并不会把指向该内存的指针设置为nullptr
。在delete
之后,任何还在引用被删除对象的指针现在都是悬挂指针,对其的使用是未定义的行为。为了避免这种情况,一种常见的做法是在delete
一个指针之后,立即把这个指针设置为nullptr
。
- 调用析构函数:对于类类型的对象,编译器首先调用对象的析构函数。这是为了执行任何必要的清理工作,例如释放对象可能拥有的其他资源(如动态分配的内存,打开的文件句柄等)。对于基本类型(如
2.2 使用方式
- new和delete不仅分配和释放内存,还会调用对象的构造函数和析构函数,malloc和free仅分配和释放内存,不调用构造函数和析构函数
- new自动计算所需分配的内存,malloc需要手动计算所需分配的内存
- new和delete是类型安全的。使用new创建对象时,需要指定对象的类型,new会返回正确类型的指针,malloc返回的是
void*
,需要手动转换为正确的类型 - delete释放内存时需要该对象类型的指针,free可以接受任何类型的指针,包括
void*
- new分配失败会抛出异常,malloc分配失败会返回NULL
- new是在free store上分配内存,malloc是在heap上分配内存
- new和delete运算符可以用于分配和释放任何数据类型的内存,包括数组,malloc和free主要用于分配和释放一块大小已知的内存
- delete、free调用后,内存不会被立刻释放,指针也不会指向空,为了避免空悬指针,释放内存后,应该要把指针指向null
3. static关键字
-
在函数体,代码块或类中声明变量:当在这些上下文中使用时,
static
意味着该变量的生命周期为程序的整个执行期间,而不是仅仅是其所在的代码块。这样的变量只会被初始化一次,然后在后续的函数调用或代码块执行中保持其值。这对于需要跨多次函数调用或代码块执行保持状态的情况很有用。void some_function() { static int call_count = 0; call_count++; std::cout << "Function has been called " << call_count << " times\n"; }
-
在类中声明变量:在类中,
static
关键字用于声明静态成员变量。静态成员变量不是类的每个实例的一部分,而是属于类本身的。所有实例共享同一个静态成员变量。这种静态成员变量是与类本身关联的,而不是与类的任何特定对象关联的。因此,静态成员变量在所有类的对象中都是共享的。静态成员变量必须在类外部初始化。class SomeClass { public: static int static_member; }; SomeClass::static_member = 0;
-
在类中声明函数:在类中,
static
关键字也可以用于声明静态成员函数。静态成员函数可以在没有类的实例的情况下调用,也就是说,它们不依赖于特定的对象。静态成员函数只能访问类的静态成员变量,不能访问类的非静态成员,因为静态成员函数没有this指针。class SomeClass { public: static void static_member_function() { std::cout << "This is a static member function.\n"; } };
-
在全局变量或函数前:当
static
关键字用在全局变量或函数前时,它会更改这些实体的链接属性,使它们在其定义的源文件内部可见,而在文件外部则不可见。这被称为内部链接。这可以防止名称冲突,并帮助保持全局命名空间的清洁。// File: main.cpp static void some_function() { // This function is only visible within main.cpp }
4. strcpy、sprintf、memcpy的区别
4.1 使用方式
strcpy
、 sprintf
、memcpy
都是在C和C++中常用的字符串和内存操作函数。
-
strcpy:
strcpy
函数用于复制字符串。它将源字符串(包括结束字符’\0’)复制到目标字符串。需要注意的是,strcpy
不检查目标缓冲区的大小,所以可能会导致缓冲区溢出的问题。因此在使用时必须确保目标缓冲区足够大以容纳源字符串。char src[50] = "example"; char dest[50]; strcpy(dest, src); // Now dest contains the string "example"
-
sprintf:
sprintf
函数用于将格式化的数据写入字符串。这个函数与printf
函数类似,只是printf
将数据写入到标准输出(通常是屏幕),而sprintf
将数据写入到字符串。与strcpy
一样,sprintf
也不检查目标缓冲区的大小,所以可能导致缓冲区溢出。如果需要防止缓冲区溢出,可以使用snprintf
函数,这个函数需要指定最大的写入字符数。char buffer[50]; int a = 10; float b = 3.14; sprintf(buffer, "a = %d, b = %.2f", a, b); // Now buffer contains the string "a = 10, b = 3.14"
-
memcpy:
memcpy
函数用于复制内存区域。它将一个源内存区域的前N个字节复制到目标内存区域。与strcpy
和sprintf
不同,memcpy
并不关心数据的内容,它只是简单地复制字节。因此,memcpy
可以用来复制任何类型的数据,包括字符串、结构、数组等。需要注意的是,memcpy
不会检查源和目标内存区域是否重叠,如果重叠,可能会导致未定义的行为。char src[50] = "example"; char dest[50]; memcpy(dest, src, strlen(src) + 1); // Now dest contains the string "example"
4.2 区别
- 实现功能
strcpy
实现字符串拷贝,遇到\0
结束sprintf
用于格式化字符串memcpy
可以实现内存块的拷贝,根据size的大小来复制
- 执行效率
memcpy
最快strcpy
次之sprintf
最慢
- 操作对象
strcpy
操作对象为字符串spintf
操作对象可以为多种数据类型memcpy
操作对象为内存地址
5. 如何避免野指针
5.1 什么是野指针
"野指针"是指针变量的一个常见问题,它指的是指向"不确定区域"的指针。在以下几种情况下,指针通常被视为野指针:
- 指针变量没有被初始化。例如,声明了一个指针变量,但没有给它赋值,这个指针就是一个野指针。
- 指针被释放后没有被置为
nullptr
。例如,如果你使用delete
或free
释放了一个指针指向的内存,但没有把指针设置为nullptr
,那么这个指针就变成了野指针。 - 指针超出了其指向的对象或数组的范围。例如,如果你有一个指向数组的指针,并且这个指针移动到了数组的末尾之后,那么这个指针就变成了野指针。
5.2 使用野指针会有什么问题
使用野指针通常会引发问题,因为它可能会导致以下几种类型的错误:
-
解引用野指针:如果你试图访问野指针指向的内存,结果通常是未定义的。在最好的情况下,你可能会得到一些随机的值。在最坏的情况下,程序可能会崩溃,因为野指针可能指向一个你的程序没有权限访问的内存区域。
-
内存泄漏:如果你丢失了一个指针(即让一个指针变成了野指针),而这个指针是唯一指向一块动态分配的内存的指针,那么你就无法再释放那块内存。这被称为内存泄漏,它会导致你的程序消耗越来越多的内存。
-
数据破坏:如果你不小心使用了野指针,可能会无意中覆盖内存中的其他数据。这可能会导致各种难以跟踪的问题,因为它可能会影响到程序中的任何部分。
5.3 如何避免
-
初始化指针:当你声明一个新的指针时,立即初始化它。如果没有具体的对象可以指向,那就初始化为
nullptr
。int* p = nullptr; // 初始化为 nullptr
-
为指针对象分配内存:在使用指针操作对象之前,确保已经为这个对象分配了内存。
p = new int; // 为指针对象分配内存 // or p = (int*)malloc(sizeof(int)); // 在C中,使用malloc为指针对象分配内存
-
检查内存分配是否成功:在使用
new
或malloc
分配内存后,需要检查指针是否为nullptr
。如果是,说明内存分配失败,这时需要进行错误处理,通常是打印错误信息并退出程序。p = new(std::nothrow) int; // 使用nothrow版本的new,如果内存分配失败,返回nullptr if (p == nullptr) { std::cerr << "Memory allocation failed.\n"; exit(EXIT_FAILURE); }
-
对空间数据置零:在C中,分配内存后,为了安全,通常需要将新分配的内存初始化为零。
p = (int*)malloc(sizeof(int)); if (p == nullptr) { fprintf(stderr, "Memory allocation failed.\n"); exit(EXIT_FAILURE); } memset(p, 0, sizeof(int)); // 将内存空间置零
-
释放后置空:当你使用
delete
或free
释放一个指针指向的内存后,立即把这个指针设置为nullptr
。这样,即使你再次错误地尝试释放这个指针,由于它现在是nullptr
,这将是一个无害的操作。delete p; p = nullptr; // 释放后置空
-
使用智能指针:在C++11及以后的版本中,你可以使用智能指针(如
std::unique_ptr
和std::shared_ptr
)来自动管理内存。智能指针会在合适的时候自动释放它们所拥有的内存,这可以避免很多与野指针相关的问题。std::unique_ptr<int> p(new int(42)); // Now you don't have to worry about deleting the memory when you're done with it
6. 引用和指针的作用及区别
6.1 作用
指针的作用:
-
传递参数:指针可以作为函数参数,允许函数访问和修改它们指向的对象。这样可以避免对象数据的复制,特别是在处理大型数据结构时,使用指针而非值传递可以显著提高性能。
void update(int* p) { *p = 10; // 修改p指向的值 }
-
实现多态:在面向对象的编程中,指针可以用来实现多态。基类的指针可以指向派生类的对象,并调用虚函数,这样在运行时就能根据对象的实际类型执行正确的函数。
class Base { public: virtual void print() const { std::cout << "Base\n"; } }; class Derived : public Base { public: void print() const override { std::cout << "Derived\n"; } }; Base* p = new Derived; p->print(); // Prints "Derived" delete p;
-
代码复用:指针可以帮助实现代码复用。例如,你可以写一个通用的函数,这个函数接受指向不同类型的对象的指针,并对这些对象执行相同的操作。
引用的作用:
-
传递参数:引用也可以作为函数参数,允许函数访问和修改它们引用的对象。使用引用参数可以避免对象的复制,同时语法上比使用指针更清晰。
void update(int& ref) { ref = 10; // 修改ref引用的值 }
-
函数返回值:函数可以返回对象的引用,这样可以避免对象数据的复制。例如,你可以返回对象的成员变量的引用,或者返回数组元素的引用。
int& getFirst(std::vector<int>& vec) { return vec[0]; // 返回vec的第一个元素的引用 }
指针和引用在很多情况下可以互换使用,但它们各自都有一些特定的用途,例如指针可以用来实现动态多态,而引用可以用来创建别名。在选择使用指针还是引用时,通常应该考虑代码的具体需求和设计意图。
6.2 区别
指针和引用在C++中都被用于间接访问对象,但它们之间存在几个关键的区别:
-
是否需要初始化:
-
指针:在声明时不一定需要初始化,但出于安全考虑,最好初始化。如果没有具体的对象可以指向,可以将其初始化为
nullptr
。int* p = nullptr; // 指针初始化为 nullptr
-
引用:必须在声明时初始化,并且一旦被初始化后,就不能改变其绑定的对象。引用不能被初始化为无对象。
int a = 10; int& ref = a; // 引用初始化,绑定到a
-
-
是否允许为空:
- 指针:可以为空,即指向
nullptr
。 - 引用:不允许为空,它必须始终绑定到一个具体的对象。
- 指针:可以为空,即指向
-
是否直接操作对象:
-
指针:通过某个指针变量指向一个对象,对它所指向的变量进行间接操作。
*p = 20; // 间接操作p所指向的对象
-
引用:是目标对象的别名,对引用操作就是直接对目标对象操作。
ref = 20; // 直接操作引用的目标对象
-
-
是否是对象:
-
指针:是一个对象,它有自己的地址,并可以被其他指针指向,即可以定义指针的指针。
int** pp = &p; // 定义指针的指针
-
引用:不是一个对象,它没有实际地址,不能定义引用的引用或引用的指针。
-
这些区别使得指针和引用在不同的场合有不同的用途。例如,如果你需要一个可以为空的、可以重新绑定的、或者可以指向不同类型对象的“间接访问”,你可能需要使用指针。而如果你需要一个始终绑定到某个对象、并且可以像操作实际对象一样操作的“间接访问”,你可能需要使用引用。