您现在的位置是:首页 >技术杂谈 >【C/C++】语言相关题型(二)网站首页技术杂谈
【C/C++】语言相关题型(二)
文章目录
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!
");
}
在 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 "; }
-
在类中声明变量:在类中,
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. "; } };
-
在全局变量或函数前:当
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
函数用于复制字符串。它将源字符串(包括结束字符’ ’)复制到目标字符串。需要注意的是,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
实现字符串拷贝,遇到