1、类的特殊成员函数的总结(3-2-1)
- 构造函数(三个构造):
- 普通构造函数:用于创建对象并初始化其成员。
- 拷贝构造函数:用已存在的对象创建新对象,进行深拷贝(若有资源管理时需自定义)。
- 移动构造函数:以右值引用为参数,转移资源所有权(如指针),避免不必要的深拷贝,提高效率。
- 赋值函数(两个赋值):
- 普通赋值函数:实现对象间的赋值操作(需处理自赋值、深拷贝等情况)。
- 移动赋值函数:通过右值引用转移资源,避免深拷贝开销,通常配合转移构造函数实现资源管理优化。
- 析构函数(一个析构):用于对象生命周期结束时释放资源(如动态分配的内存、文件句柄等),确保资源不泄漏。
2、 移动赋值函数为什么不加const
拷贝赋值函数 MyString& MyString::operator=(const MyString& ob)
参数加 const
,是为了:
- 防止意外修改原对象:拷贝赋值仅复制原对象数据,不希望修改原对象,
const
可避免函数内误操作修改原对象。 - 提高通用性:能接受常量对象作为参数,若不加
const
,则无法对常量对象进行拷贝赋值操作。
移动赋值函数 MyString& operator=(MyString&& ob)
不加 const:
- 移动语义的核心是转移资源所有权,需要修改被移动对象(如将其内部指针置空,避免后续重复释放资源)。
- 若参数加
const
,则无法修改被移动对象,导致移动语义无法实现。
3、重载与覆盖
重载(Overloading)
发生在同一作用域内(如同一个类中),允许存在多个同名函数,但参数列表必须不同(参数类型、个数或顺序不同)
class Example {
public:
void func(int a) { /*...*/ } // 重载版本1
void func(double b) { /*...*/ } // 重载版本2(参数类型不同)
void func(int a, int b) { /*...*/ } // 重载版本3(参数个数不同)
};
- 返回值类型无关:仅返回值不同不能构成重载
- 编译时静态多态:编译器根据调用时的实参类型和数量选择匹配的函数
- 无虚函数要求:普通成员函数即可重载
覆盖(Overriding)
发生在继承关系中(派生类与基类之间),派生类重新定义基类的虚函数,要求函数签名(名称、参数列表、返回类型)完全一致
class Base {
public:
virtual void show() { /*...*/ } // 基类虚函数
};
class Derived : public Base {
public:
void show() override { /*...*/ } // 覆盖基类虚函数(C++11后推荐使用override关键字)
};
- 必须虚函数:基类函数需声明为
virtual
,否则派生类同名函数为隐藏而非覆盖 - 运行时多态:通过基类指针或引用调用时,根据对象实际类型决定执行哪个版本
- 协变返回类型:派生类返回类型可以是基类返回类型的派生类
当派生类重写基类的虚函数时,允许返回基类方法返回类型的派生类对象。
- Liskov替换原则:派生类对象可替代基类对象使用
- 隐式类型转换:
Dog*
可隐式转换为Animal*
,但反向转换需显式处理// 基类 class Animal { public: virtual Animal* clone() const { // 返回基类指针 return new Animal(*this); } virtual void speak() const { std::cout << "Animal sound" << std::endl; } }; // 派生类 class Dog : public Animal { public: // 协变返回类型:返回派生类指针 Dog* clone() const override { return new Dog(*this); } void speak() const override { std::cout << "Bark!" << std::endl; } }; int main() { Animal* animal1 = new Dog(); Animal* clonedAnimal = animal1->clone(); // 实际调用Dog::clone() clonedAnimal->speak(); // 输出 "Bark!"(无需类型转换) delete animal1; delete clonedAnimal; return 0; }
若派生类定义了与基类同名但参数不同的函数(无论基类是否为虚函数),基类函数会被隐藏,而非覆盖
class Base { public: void func(int a) {} };
class Derived : public Base {
void func(double b) {} // 隐藏基类的func(int),而非覆盖
};
C++11引入override
关键字,强制检查派生类函数是否真正覆盖基类虚函数,避免因参数不匹配导致的隐藏错误
4、C++支持函数重载而C语言不支持的原因
C++通过编译器对函数名进行"名称修饰(Name Mangling)"实现重载:
- 根据函数参数类型、个数、顺序(容易忽略)生成唯一修饰名
- 调用时通过符号表匹配正确函数实现
- C语言不做名称修饰,要求函数名全局唯一,因此无法区分同名不同参的函数
5、函数重载注意事项
- 必须满足参数列表差异(类型/个数/顺序)
- 返回值类型不能作为重载依据(会导致编译错误)
- 同名函数在不同作用域不构成重载
- 注意隐式类型转换可能引发的二义性:
void func(int a) {}
void func(double b) {}
func(5); // 明确调用int版
func(5.0); // 明确调用double版
func('c'); // 调用int版(char→int隐式转换)
6、C/C++代码互调用规范
extern "C" { // C++ 调用C
#include "c_header.h"
}
// cpp_code.cpp
extern "C" void cpp_func() { /*...*/ }
在头文件中通过条件编译实现跨语言兼容:
#ifdef __cplusplus
extern "C" {
#endif
void shared_function();
#ifdef __cplusplus
}
#endif
7、const 不影响重载
void func(int a) {}
void func(const int a) {}
编译器会认为这是两个完全相同的函数,因为:
- 当参数是按值传递时,
const
限定符对函数的行为没有实际影响 - 编译器只关心参数的"底层类型"(underlying type)
- 对于按值传递的参数,
const int
和int
被认为是相同的类型
这会导致编译错误,提示重复定义了同一个函数。
int func(int* a); // 指针可变,内容可变
int func(const int* a); // 指针可变,内容不可变
int func(int* const a); // 指针不可变(实际声明时与int* a等效)—— 报错
int func(const int* const a); // 指针/内容均不可变(需在定义中保持const)
int* const
是顶层 const(指针本身为 const),C++ 在函数重载解析时忽略顶层 const,视为int*
,与第一个函数参数类型相同,导致冗余声明const int* const
包含顶层 const(指针本身为 const,被忽略)和底层 const(指向 const 对象,保留),最终参数类型为const int*
。与int func(const int *a);
存在重复声明
8、左值引用与右值引用
引用必须初始化,指针可以不初始化。
汇编层面没有引用与指针之间的区分。对引用赋值,等价于底层指针解引用。
引用只有一级引用,没有多级引用。
一个右值引用变变量,本身是一个左值。右值引用只能用来引用右值。
int *p1 = &arr;
❌
- 问题本质:类型不匹配
arr
作为数组名时,隐式转换为int*
(指向首元素的指针)&arr
的类型是int(*)[10]
(指向整个数组的指针)- 试图将
int(*)[10]
赋值给int*
类型指针,类型不兼容
int &q = arr;
❌
- 问题本质:引用绑定规则冲突
- 数组名
arr
是不可修改的左值(代表数组整体) - 引用必须绑定到具体对象,而数组名不能作为单个对象的引用
- 正确做法是绑定到数组元素:
int &q1 = arr[0];
- 数组名
&arr
的类型正好匹配 int(*)[10]
9、形参设置默认值
给形参设置默认值时,必须按照从右向左的顺序赋值,不能间隔或从左到右
函数的默认参数既可以在函数定义处指定,也可以在函数声明处指定。
无论是在定义处还是声明处,同一个形参的默认值只能指定一次。如果同时在多处为同一参数提供默认值,会导致编译错误。
默认参数在 编译期替换(编译器直接将默认值写入调用代码),无运行时开销。编译器将默认参数视为编译期常量,直接嵌入调用代码,与程序员显式书写的参数在机器码层面完全等价。
10、strlen和sizeof
strlen
运行期计算, size_t strlen(const char *s)
,仅接受char*
类型(字符串),只和字符串有关。
从字符串首地址开始,逐个字符检查,直到遇到 '\0'
(空字符,字符串结束标志) 时停止。所以算字符串长度的时候,要思考是不是还需要再加上最后的 '\0'
空字符。
仅计算'\0'
前的字符(不包含'\0'
,需字符串正确终止)
#include <stdio.h>
#include <string.h>
int main() {
char str1[] = "hello\nworld"; // 包含'\n'
char str2[] = "abc\0def"; // 提前终止('\0'前的字符计数)
printf("str1长度:%zu\n", strlen(str1)); // 输出11(h,e,l,l,o,\n,w,o,r,l,d,共11个字符)
printf("str2长度:%zu\n", strlen(str2)); // 输出3(a,b,c,遇到'\0'停止)
return 0;
}
sizeof
编译期计算,结果为 size_t
(字节数,如数组总内存)
sizeof 管内存物理大小(编译期、安全、含所有字节)
包含所有分配的内存(如数组的全部字节,含'\0'
)