一、动态内存分配
在C++中,所有内存需求都是在程序执行之前通过定义所需的变量来确定的,但是可能存在程序的内存需求只能在运行时确定的情况,例如,当需要的内存取决于用户输入。在这些情况下,程序需要动态分配内存。
动态内存分配是指在程序执行过程中,根据需要从系统中动态地分配或回收存储空间的方法。
这种方式与数组等静态内存分配方法不同,后者需要在程序声明部分定义,并且在函数结束时才释放。
1.new和delete运算符
①new运算符用于为单个对象或对象数组分配内存。它的一般语法如下:
typeName *pointer = new typeName(initializer);
--pointer是指向新分配内存的指针。
--typeName是要分配的对象类型。
--initializer是可选的,用于初始化新分配的对象,如果不需要初始化,可以省略initializer。
对于数组,语法如下:
typeName *pointer = new typeName[arraySize];
②delete用于释放动态分配的内存。它的一般语法是:
delete pointer;
对于数组,语法是:
delete[] pointer;
当使用delete释放了内存后,指针将变为空指针。释放内存后,指针建议设置为nullptr,以明确表示指针不再指向任何有效内存。
int* p = new int(5);
delete p;
p = nullptr;
double* arr = new double[10];
delete[] arr;
arr = nullptr;
例如,分配一个整数并初始化为5,分配一个包含10个double类型数据的数组,将它们释放并将指针置为空。
2.内存管理机制
在C++中,内存分配由堆和栈组成。堆是动态分配的,由程序员控制分配和释放;栈由编译器自动管理,用于存储函数调用时的临时信息。
为了避免手动管理内存可能导致的内存泄漏,C++提供了智能指针来帮助管理动态分配的内存。智能指针包括shared_ptr、unique_ptr和weak_ptr,它们提供了不同的内存管理策略和所有权模型。
3.注意事项
- 不要使用delete释放同一内存块两次。
- 不要使用delete来释放不是new分配的内存。
- 如果使用new[]为数组分配内存,则应使用delete[]来释放。
- 动态分配的内存应该在不再需要时及时释放,以避免内存泄漏。
- new和delete在C++中是类型安全的,不需要显式类型转换。
- 对空指针应用delete是安全的。
- 尽管C++支持C风格的动态内存分配函数malloc和free,但推荐使用new和delete,因为它们提供了更高级的特性,如对象构造和析构的自动调用。
二、函数提高
1.函数默认参数
在C++中,函数形参列表中的形参是可以有默认值的。
语法:返回值类型 函数名 (数据类型 参数 = 默认值) { }
int func1(int a, int b, int c = 10) {
return a + b + c;
}
int a = 2, b = 3, c = 5;
cout << func1(a, b) << " ";
cout << func1(a, b, c) << endl;
//输出15 10
如果某个位置已经有了默认参数,那么从这个位置往后都要有默认参数。
如果函数声明有默认值,函数实现的时候就不能有默认参数。声明和实现只能有一个写默认参数,否则会出现二义性。
2.函数占位参数
C++中函数的形参列表里可以有占位参数用来占位,调用函数的时候必须填补该位置。
语法:返回值类型 函数名 (数据类型){ }
占位参数还可以有默认参数。
void func1(int a, int) {
}
void func2(int a, int = 10) {
}
func1(2,3); //这个3传进去是拿不到的
func2(5);
2.函数重载
C++函数重载通过在同一作用域内声明多个同名函数,但具有不同的参数列表(包括参数的个数、类型或顺序)来实现的。
这种机制允许使用相同的函数名来处理不同的数据类型或执行不同的操作,从而提高了程序的灵活性和可读性。
函数重载满足条件:
- 同一个作用域下
- 函数名相同
- 函数参数类型不同、数量不同或者顺序不同
注意:函数的返回值类型不同不可以作为函数重载的条件。
void func(int a) {
cout << "func(int a)调用" << endl;
}
//参数类型不同
void func(double a) {
cout << "func(double a)调用" << endl;
}
//参数个数不同
void func(int a, double b) {
cout << "func(int a, double b)调用" << endl;
}
//参数顺序不同
void func(double a, int b) {
cout << "func(double a, int b)调用" << endl;
}
func(5); //func(int a)调用
func(2.0); //func(double a)调用
func(5, 2.0); //func(int a, double b)调用
func(2.0, 5); //func(double a, int b)调用
函数重载遇到函数默认参数:
void func(int a) {
}
void func(int a, double b = 10.0) {
}
func(5);
//编译器不知道调用哪个,出现二义性
//要避免这种情况出现
函数重载引用参数:
void func(int& a) {
cout << "func(int& a)调用" << endl;
}
void func(const int& a) {
cout << "func(const int& a)调用" << endl;
}
void func(int&& a) {
cout << "func(int&& a)调用" << endl;
int x = 5;
const int y = 10;
func(x); //func(int& a)调用
func(y); //func(const int& a)调用
func(x + y); //func(int&& a)调用
C++将调用最匹配的版本,使得我们可以根据参数是左值、const还是右值来制定函数行为。
如果没有定义函数void func(int&& a),func(x + y)将调用函数void func(const int& a)。
3.函数传参方式
①按值传递:
按值传递是最基本的传参方式,它将实参的副本传递给函数。在函数内部对参数的修改不会影响到原始的实参。这种方式适用于基本数据类型和小型对象,因为它简单且成本较低。但是,如果传递的是大型对象,按值传递会涉及到对象的拷贝,可能导致性能问题。
②指针传递:
指针传递将实参的地址传递给函数,函数可以通过这个地址来修改实参指向的数据。这种方式可以避免拷贝大型对象,但使用指针可能会增加程序出错的可能性,尤其是处理空指针或野指针时。
③引用传递:
引用传递将实参的引用传递给函数,函数可以像操作普通变量一样直接操作实参。引用传递可以避免拷贝,并且语法上与按值传递相似,易于理解和使用。它适合于需要在函数内部修改参数的场合。
④右值引用传递:
右值引用允许函数专门针对右值(即将被销毁的临时对象)进行优化。通过右值引用,可以实现移动语义,即将资源从一个对象转移到另一个对象,而不是进行拷贝,从而提高效率。
⑤const引用传递:
const引用传递是一种特别的引用传递方式,它保证了函数不会修改引用的对象。这种方式结合了引用传递的效率和const的安全性,适用于需要读取但不需要修改参数的情况。
对于使用传递的值而不作修改的函数:
- 如果数据对象很小,如内置数据类型或小型结构体,则按值传递。
- 如果数据对象是数组,则使用指针,因为这是唯一的选择,并将指针声明为指向const的指针。
- 如果数据对象是较大的结构体,则使用const指针或const引用,以提高程序的效率。这样可以节省复制结构体所需的时间和空间。
- 如果数据对象是类对象,则使用const引用。类设计的语义常常要求使用引用。因此,传递类对象参数的标准方式是按引用传递。
对于修改调用函数中数据的函数:
- 如果数据对象是内置数据类型,则使用指针。如果看到诸如fixit(&x)这样的代码(其中x是int),则很明显,该函数将修改x。
- 如果数据对象是数组,则只能使用指针。
- 如果数据对象是结构体,则使用引用或指针。
- 如果数据对象是类对象,则使用引用。
当然,这只是一些指导原则,很可能有充分的理由做出其他的选择。例如,对于基本类型,cin使用引用,因此可以cin>>n,而不是cin>>&n。
4.函数指针
函数指针是C++中的一种特殊类型的指针,它指向函数的内存地址。通过函数指针,可以在运行时决定调用哪个函数,这使得程序更加灵活和模块化。函数指针可以作为参数传递给其他函数,或者作为函数的返回值,实现回调机制和策略模式等设计模式。
①定义和初始化函数指针
函数指针的定义格式为:返回类型 (*函数指针变量名)(参数列表);
通常,要声明指向特定类型的函数的指针,可以先编写这种函数的原型,然后用(*pf)替换函数名。这样pf就是这类函数的指针。
为提供正确的运算符优先级,必须在声明中使用括号将*pf括起。括号的优先级比*运算符高,因此
*pf(int)意味着pf()是一个返回指针的函数,而(*pf)(int)意味着pf是一个指向函数的指针。
举例:
int func(int a, int b) {
return a + b;
}
int (*pf)(int, int);
pf = func; //相当于 pf = &func;
int res = pf(2, 3); //相当于 (*pf)(2, 3);
cout << res << endl;
//输出5
定义一个指向返回int并接受两个int参数的函数指针,通过函数名初始化,不需要带上函数的参数列表,可以通过解引号*或者省略解引用直接使用函数指针的名称。
typedef int (*ptr_func)(int, int);
ptr_func pf = func;
创建类型别名,将函数指针的名称简化为pf。
int func1(int a, int b) {
return a + b;
}
int func2(int a, int b) {
return a * b;
}
int function(int(*pfunc)(int, int), int a,int b) {
return pfunc(a, b);
}
cout << function(func1, 2, 3) << " ";
cout << function(func2, 2, 3) << endl;
//输出5 6
int (*arrpfunc[2])(int, int) = { func1,func2 };
cout << arrpfunc[0](2, 3) << " ";
cout << arrpfunc[1](2, 3) << endl;
//输出5 6
使用函数指针实现两个数相加或相乘,并在调用时选择想要调用的函数。
通过调用函数指针数组中的不同元素,实现对不同操作的选择性调用。
typedef double(*pfunc)(double);
int function(pfunc pf, double x) {
return pf(x);
}
const double pi = atan(1.0) * 4.0;
cout << pi << " ";
cout << function(sin,pi/2) << " ";
cout << function(cos,pi/2) << endl;
//输出3.14159 1 0
使用函数指针测试数学函数sin和cos,并使用typedef简化函数参数。
②注意事项
- 函数指针的类型必须与其指向的函数的签名完全匹配,包括返回类型和参数列表。
- 使用函数指针时,需要注意指针的有效性,确保它指向一个有效的函数地址。
- 在多线程环境中,函数指针的使用需要额外小心,以避免潜在的并发问题。
函数指针常用于实现回调函数,即在程序中动态指定要调用的函数。例如,在处理图形用户界面(GUI)事件时,可以将事件处理函数通过函数指针传递给事件循环,从而在用户执行特定操作时调用相应的处理函数。
5.内联函数
C++内联函数是一种特殊的函数,通过使用inline关键字来定义。内联函数的主要目的是提高程序的执行效率,通过在编译阶段将函数体代码直接嵌入到调用该函数的地方,从而避免了函数调用的开销,如参数传递、栈空间的进栈与出栈等。
inline int add(int a, int b) {
return a + b;
}
①使用场景:
- 内联函数适用于代码量小、调用频繁的函数。通常,如果函数的代码行数少于10行,且调用次数较多,可以考虑将其声明为内联函数。
- 类的成员函数可以被声明为内联,以减少对象模型的开销。
- inline关键字在模板函数中尤为重要,因为模板在编译时被实例化多次,使用inline可以确保在每个使用模板的地方都有正确的代码。
②内联函数与宏定义的区别:
- 内联函数具有类型检查和安全检查,而宏定义没有。宏定义在预处理阶段进行简单的文本替换,不进行类型检查,可能导致难以发现的错误。
- 内联函数可以像普通函数一样进行优化,而宏定义则不能。编译器可以对内联函数进行常规的优化,如死代码消除、循环展开等。
- 内联函数的调试信息通常比宏定义更有用,因为内联函数在编译时展开,调试器可以看到实际的函数调用和参数。
如果使用宏定义执行了类似函数的功能,应考虑将它们转换为的内联函数。
③内联函数的限制:
- 代码膨胀:内联函数可能会导致代码量增加,因为每个调用点都会插入函数体的代码。这可能会增加程序的存储空间需求。
- 编译时间:内联函数的使用可能会增加编译时间,因为编译器需要处理更多的代码替换工作。
- 递归函数:递归函数通常不能被内联,因为无法在调用点完全展开。
- 复杂函数:如果函数体过于庞大或包含复杂控制语句(如循环、递归、switch语句),编译器可能会拒绝内联。
应有选择地使用内联函数。如果执行函数代码的时间比处理函数调用机制的时间长,则节省的时间将只占整个过程的很小一部分。如果代码执行时间很短,则内联调用就可以节省非内联调用使用的大部分时间。另一方面,由于这个过程相当快,因此尽管节省了该过程的大部分时间,但节省的时间绝对值并不大,除非该函数经常被调用。
三、枚举
枚举是一种用户定义的数据类型,它由一组命名的常量组成,这些常量代表了变量可能的取值。
1.枚举的初始化
enum 枚举类型名 { 枚举常量表列 };
①默认情况下,枚举值从0开始,后面依次加1。
enum Color { RED, BLUE, GREEN };
Color是枚举类型的名称,RED、BLUE和GREEN是枚举常量,它们默认对应的整数值分别为0、1和2。
②可以手动指定默认值,全部指定或者部分指定,未被初始化的枚举值默认比它前面的枚举值大1。默认值也可以重复指定。
enum Color { RED, BLUE = 5, GREEN, BLACK = 0 };
RED、 BLUE、GREEN、BLACK对应整数值分别为0、5、6、0。
③自定义类型
enum Color :unsigned char { RED, BLUE, GREEN, BLACK };
指定潜在类型为unsigned char,枚举常量的值不能超过unsigned char的最大值。
不能指定为float或者double等类型,因为枚举量必须是一个整数。
2.枚举对象的声明和赋值
可以先声明再赋值,也可以定义时声明并赋值。
enum Color { RED, BLUE, GREEN, BLACK }c1 = BLUE;
Color c2 = BLACK;
注意:只能把枚举常量赋予枚举类型的变量,不能把枚举常量对应的整数值直接赋予枚举变量。
enum Color { RED, BLUE = 5, GREEN, BLACK };
Color c1 = BLACK; //c1=7
Color c2 = Color(4); //强制类型转换,c2=4
//c1 = 3; //错误
int a = RED; //隐式转换,a=0
3.枚举的特点
- 枚举常量是常量,不能在定义之外的任何地方用赋值语句对它赋值。
- 枚举元素不是字符常量也不是字符串常量,使用时不要加引号。
- 枚举常量不是字符串,不能用%s方式输出字符串。
- 枚举类型可以进行比较,枚举类型的变量之间可以使用关系运算符进行比较。
- 枚举类型的变量不能进行算术运算。
- 枚举类型是预处理指令#define的替代,可以用宏定义来完成相同的任务,但枚举可以提高效率和灵活性。
枚举和switch搭配使用:
enum Color { RED, BLUE = 5, GREEN, BLACK };
Color c1 = BLACK;
switch (c1)
{
case red:
cout << "red" << endl;
break;
case green:
cout << "green" << endl;
break;
case blue:
cout << "blue" << endl;
break;
}
4.枚举类
C++中的枚举类提供了比传统枚举更强的类型安全和作用域限制,通过在enum后面加关键字class或struct来实现。
enum class Color { RED, BLUE = 5, GREEN, BLACK };
//Color c1 = BLACK; //错误,未加作用域
Color c1 = Color::BLACK;
//int x = Color::BLACK; //错误,不支持隐式转换
特点:
- 枚举值的名称在枚举类的作用域内是局部的,不会与其他作用域中的名称冲突。
- 枚举值需要通过作用域解析运算符::来访问。
- 枚举值不会隐式转换为其底层类型,需要显式转换。
- 由于作用域的限制和禁止隐式转换,枚举类提供了更强的类型安全。