1、static的作用
1)修饰局部变量
概念:当使用static
关键字修饰局部变量时,该变量就变成了静态局部变量。这意味着它不再是传统的自动存储期变量,而是具有静态存储期。
作用域:静态局部变量的作用域确实没有变化,它仍然是在其定义的局部范围内,即它所在的代码块(例如函数体)内部。在作用域外,该变量是不可见的。
生存期:静态局部变量的生存期与全局变量类似,都是全局的。这意味着静态局部变量在程序开始执行时就已经存在,并且直到程序结束才会被销毁。这与普通的局部变量不同,普通局部变量在函数被调用时创建,在函数返回时销毁。
特点:静态局部变量的初始化只会在第一次进入包含它的函数时发生。之后,每次函数被调用时,该变量都会保留上一次函数返回时的值,而不是被重新初始化。这是静态局部变量与普通局部变量之间的主要区别。
插曲:
外部链接:
- 外连接允许其他源文件或库访问的函数和变量。如果一个名称(如函数或变量)对编译单元(.cpp文件)来说不是局部的,那么在链接的时候,其他的编译单元可以访问它,也就是说它可以和别的编译单元交互。
- 当使用
extern
关键字标记函数或变量时,它们就具有外连接。这意味着这些函数或变量可以在其他源文件中使用。例如,在一个源文件中定义了一个全局变量或函数,并在另一个源文件中使用extern
关键字声明它,那么后者就可以访问前者定义的变量或函数。
内部链接:
- 内连接意味着函数和变量只能在定义它们的源文件中使用。如果一个名称对编译单元来说是局部的,那么在链接的时候,其他的编译单元无法链接到它,且不会与其他编译单元中的同名标识符相冲突。
- 默认情况下,如果没有使用
extern
标识符,函数和变量将具有内连接。此外,使用static
关键字也可以明确指定内连接。这意味着即使在其他源文件中定义了具有相同名称的函数或变量,编译器也不会产生冲突,因为每个源文件都有自己的独立作用域。
2)修饰全局变量
概念:
当static
修饰全局变量时,该全局变量的链接属性从外部链接变为内部链接。这意味着该全局变量仅在其定义的源文件中可见,而在其他源文件中是不可见的。
作用域:
全局变量的作用域是整个程序,包括所有的源文件。但是,当全局变量被static
修饰后,其作用域并没有改变,仍然是全局的。但是,它的可见性被限制在了定义它的源文件中。
链接属性:
通常,全局变量具有外部链接属性,这意味着它们可以在多个源文件之间共享和访问。但是,当全局变量被static
修饰后,其链接属性变为内部链接,这意味着该变量只在定义它的源文件中可见,其他源文件即使使用相同的变量名也不会冲突。
因此,static
修饰全局变量的主要作用是限制其可见性,确保它在其他源文件中不会被误用或产生命名冲突。这对于创建只在特定源文件中使用的辅助变量或计数器非常有用。
3)修饰函数
概念:
静态函数是指使用static
关键字修饰的函数。它与普通函数的主要区别在于其链接属性。
作用域:
静态函数的作用域与其定义位置有关。如果是在文件作用域(全局作用域)定义的静态函数,则它的作用域是全局的,即在整个程序中可见。但是,由于它是静态的,因此它的链接属性是内部链接,这意味着它只在其定义的源文件中可见,其他源文件无法链接到它。
如果是在局部作用域(如函数内部)定义的静态函数,这是不合法的,因为C++不允许在局部作用域中定义静态函数。
链接属性:
通常,非静态函数具有外部链接属性,这意味着它们可以在多个源文件之间共享和访问。但是,当函数被static
修饰后,其链接属性变为内部链接。这保证了即使在其他源文件中存在同名的函数,也不会发生链接时的冲突或混淆。
总结:
使用static
修饰函数的主要目的是限制函数的可见性,确保它只在定义它的源文件中被使用。这对于创建只在特定源文件中使用的辅助函数或隐藏实现细节非常有用。通过这样做,可以减少命名冲突的风险,并提高代码的可维护性和模块化。
需要注意的是,静态函数仍然可以被同一源文件中的其他函数或全局变量调用,但它们对于其他源文件是不可见的。
4) 修饰类的成员函数
您关于static
修饰类的成员的描述是准确的。当static
关键字用于修饰类的成员时,无论是数据成员还是成员函数,该成员就成为静态成员。以下是关于static
修饰类的成员的详细解释:
概念:
静态成员是类的所有对象共享的成员。它们不属于类的任何特定实例,而是属于类本身。静态成员可以是数据成员(变量)或成员函数。
数据成员:
静态数据成员为类的所有对象共享一个存储空间。无论创建多少个类的对象,都只有一个静态数据成员的副本。静态数据成员可以在类的外部进行初始化,并且可以通过类名和作用域解析运算符(::)来访问,而不需要创建类的对象。
class MyClass { | |
public: | |
static int count; // 静态数据成员声明 | |
}; | |
int MyClass::count = 0; // 静态数据成员定义和初始化 |
在上面的例子中,count
是一个静态数据成员,它可以在没有MyClass对象的情况下被访问和修改。
成员函数:
静态成员函数只能访问静态成员(包括静态数据成员和其他静态成员函数),而不能直接访问类的非静态成员。这是因为静态成员函数不与类的任何特定对象关联,因此没有this
指针。静态成员函数可以通过类名和作用域解析运算符来调用。
class MyClass { | |
public: | |
static int count; | |
static void incrementCount() { // 静态成员函数 | |
count++; | |
} | |
}; | |
| |
int main() { | |
MyClass::incrementCount(); // 调用静态成员函数 | |
std::cout << MyClass::count << std::endl; // 输出静态数据成员的值 | |
return 0; | |
} |
在上面的例子中,incrementCount
是一个静态成员函数,它用于增加静态数据成员count
的值。由于它是静态的,因此可以通过类名MyClass
直接调用,而不需要创建MyClass的对象。
静态成员在类的所有对象之间共享它们的值,这对于统计对象数量、维护类级别的状态或实现与类本身相关而不是与特定对象相关的功能非常有用。它们提供了一种在类的所有实例之间共享信息的方式,而无需在每个实例中复制该信息。
2、C/C++中const的用途?
在C和C++编程语言中,const
是一个非常重要的关键字,主要用于定义常量,即不可变的变量。
-
定义常量:
const
可以用于定义常量,这些常量的值在初始化后不能被修改。这有助于创建只读的变量,增加代码的可读性和可维护性。 -
指针和
const
:const
可以与指针一起使用,以限制指针本身或指针所指向的内容的修改。-
指向常量的指针:指向常量的指针不能用于修改它所指向的值。const int *p;
-
常量指针:常量指针的值(即它所指向的地址)在初始化后不能被修改。int *const p;
- 指向常量的常量指针:这样的指针既不能修改它所指向的值,也不能修改它自己的值(即它指向的地址)。const int *const p;
-
-
函数参数:将
const
用于函数参数可以确保参数在函数体内不会被修改。这既可以保护传入的数据,也可以让调用者知道这个函数不会修改其参数。 -
类成员:
const
可以用于类的成员变量或成员函数,以限制其修改。- 常量成员变量:常量成员变量只能在类的构造函数初始化列表中初始化,之后不能被修改。
- 常量成员函数:常量成员函数不能修改类的任何成员变量(除非它们被声明为
mutable
)-
不能修改成员变量:当一个成员函数被声明为
const
时,它不能修改类的任何非static
且非mutable
的成员变量。这是因为const
成员函数保证在调用该函数时不会改变对象的任何状态。 -
不能调用非const成员函数:如果一个成员函数被声明为
const
,那么它不能调用类的任何非const
成员函数,因为非const
成员函数可能会修改成员变量,从而违反const
成员函数的承诺。 -
只能被const对象调用:一个
const
对象只能调用其const
成员函数。这是因为const
对象的内容是不可变的,因此只能调用那些不会修改对象状态的成员函数。 -
const与static:
const
和static
关键字确实不能同时用于修饰成员函数。因为static
成员函数不依赖于类的任何特定实例,它们属于类本身,因此没有this
指针。而const
成员函数则需要一个this
指针来确保不修改对象的任何成员变量。因此,它们的用途和语义是互斥的。
-
-
提高程序的可读性和可维护性:使用
const
可以清晰地表明哪些变量或参数是只读的,从而帮助其他程序员更好地理解代码的功能和预期行为。 -
const 修饰函数返回值:和修饰指针和变量的作用一样。
-
const修饰类对象:
- 对象的任何成员都不能被修改。
- 只能调用const成员函数。
3、new和malloc的区别?
相同点:new和malloc都是在动态内存分配时使用的,它们的基本功能相同,都是在程序运行期间从堆中分配一段内存空间。
不同点:
- 语法:new是C++特有的运算符,而malloc是C语言中的函数。
- 类型处理:new在创建并初始化对象时比malloc更便捷。new会自动调用构造函数并返回指向对象的指针,而malloc只是返回分配空间的地址,不会调用构造函数。此外,new运算符在开辟内存时需要指定类型,而malloc所分配的内存大小则是以字节为单位,且malloc的返回值需要强转成指定类型的地址。
- 异常处理:new在无法为程序提供所需大小的内存时会抛出bad_alloc异常,而malloc在内存不足的情况下只会返回null指针,不会抛出异常。
- 内存分配:new不需要显式指定内存大小,在C++中,使用new操作符创建对象时不需要显式指定要分配的内存大小。编译器会根据对象的类型自动计算所需的大小,并调用相应的构造函数进行初始化,会分配一个足以存储int类型数据的内存空间,并返回指向这个空间的指针。。malloc需要显式指定内存大小,C语言中的malloc函数要求程序员显式指定要分配的内存大小(以字节为单位)。需要注意的是,malloc的返回值是void*,因此需要显式转换为所需类型的指针。
- 效率:在某些情况下,new的效率可能会稍低于malloc。这是因为new在执行时会先调用malloc分配内存,然后再调用对象的构造函数。而malloc只负责分配内存,不涉及构造函数的调用。
- 内存位置区别:malloc是从堆上动态地为对象分配内存,而new是从自由存储区上为对象动态地分配内存。自由存储区是C++基于new操作符的一个抽象概念,通过new操作符进行内存申请的内存称为自由存储区。自由存储区的位置取决于operator new的实现细节,它不仅可以是堆,也可以是静态存储区。
4、new和delete的实现原理,delete是如何实现释放内存的?
new
和 delete
是 C++ 中用于动态内存分配和释放的操作符。它们的实现原理依赖于底层的内存管理机制,通常涉及堆内存的分配和回收。
new的实现原理:
- 分配内存:
new
操作符首先调用底层的内存分配函数(通常是operator new
),在堆上分配足够大小的内存空间。这个大小是根据要创建的对象类型计算得出的。 - 调用构造函数:分配内存成功后,
new
会调用对象的构造函数来初始化这块内存区域。这确保了对象在第一次使用前处于有效状态。
delete的实现原理:
- 调用析构函数:
delete
操作符首先调用对象的析构函数。析构函数负责执行对象销毁前的清理工作,比如释放对象持有的资源、关闭文件等。 - 释放内存:析构函数执行完毕后,
delete
调用底层的内存释放函数(通常是operator delete
)来释放之前通过new
分配的内存空间。这个过程将内存标记为可用,以便后续的内存分配请求可以使用。
注意事项:
- 使用
new
分配的内存必须使用delete
来释放,否则会导致内存泄漏。 - 对于数组,应使用
new[]
和delete[]
来分配和释放内存,以确保正确的内存管理。 - 重载
operator new
和operator delete
可以自定义内存分配和释放的行为,但需要注意正确地管理内存,避免引入新的问题。
new
和 delete
的实现原理依赖于底层的内存管理机制,它们通过调用构造函数和析构函数以及底层的内存分配和释放函数来确保对象的正确创建和销毁。
5、malloc和free的实现原理?
malloc
和 free
是 C 语言中用于动态内存分配和释放的函数。
malloc实现原理:
- 查找合适的内存块:当调用
malloc
时,它会向操作系统请求分配指定大小的内存。首先,它会在一个称为堆(heap)的内存区域中查找一个足够大的空闲内存块。这个查找过程可能涉及遍历一个数据结构(如链表或树),该数据结构记录了堆中所有已分配和未分配的内存块的信息。 - 内存块分割:如果找到一个足够大的空闲内存块,
malloc
会将其分割为两部分:一部分用于满足当前的请求(即返回给调用者),另一部分作为新的空闲内存块保留在堆中。这个分割过程可能会涉及更新堆中的数据结构。 - 返回内存地址:最后,
malloc
返回指向新分配的内存块的指针。这个指针可以被用来在程序中存储数据。
free实现原理:
- 合并空闲内存块:当调用
free
时,它会接收一个之前通过malloc
分配的内存块的指针。free
的任务是释放这块内存,使其可以被其他程序或后续的malloc
调用使用。为了实现这一点,free
会将这块内存标记为未分配状态,并更新堆中的数据结构。此外,如果这块内存与相邻的内存块都是未分配的,free
可能会将它们合并为一个更大的空闲内存块。 - 返回内存给操作系统(可选):在某些情况下,如果释放的内存块很大或者堆中的空闲内存过多,
free
可能会选择将部分或全部空闲内存返回给操作系统。然而,这并不是必须的,因为操作系统通常有自己的内存管理机制来处理未使用的物理内存。
6、头文件的ifndef/define/endif是干什么用的?与program once的区别?
共同点:防止头文件被重复包含,提高编译效率的。
不同点:
#ifndef
:这个指令检查某个宏是否已经定义。如果没有定义,那么接下来的代码(直到对应的#endif
或另一个条件编译指令)会被包含在内。#define
:这个指令用于定义一个宏。它通常用于定义一个唯一的标识符,以确保头文件的内容只被包含一次。#endif
:这个指令标志着#ifndef
或其他条件编译指令的结束
举例:
#ifndef MY_HEADER_FILE_H
#define MY_HEADER_FILE_H
// 头文件的内容 // ...
#endif // MY_HEADER_FILE_H
7、构造函数和析构函数能否抛出异常?
构造函数和析构函数都可以抛出异常。
- 构造函数抛出异常:
在C++中,如果构造函数在对象创建过程中抛出异常且未被捕获,那么程序将终止,并且任何已经成功构造的对象都会被析构。这意味着,如果构造函数抛出异常,那么它必须确保所有已分配的资源都被适当地清理。这通常是通过在构造函数中使用异常安全的代码和RAII(资源获取即初始化)技术来实现的。
在C#中,构造函数抛出异常通常是可以接受的,但开发者需要确保正确处理这些异常,并考虑它们对程序状态的影响。
- 析构函数抛出异常:
析构函数的主要职责是清理对象在生命周期中分配的资源。因此,析构函数通常不应该抛出异常。如果析构函数抛出异常且未被捕获,那么程序的行为将是未定义的,并且可能导致资源泄漏或其他严重问题。在C++中,析构函数应设计为不抛出异常,或者至少应确保异常