一,重载
1.重载概念:函数名一样,形参列表不同
2.函数重载:
- 函数名相同
- 行参列表不同(参数类型,参数个数,参数顺序)
- 返回类型不影响重载(系统没有提取返回类型,只提取函数名与行参列表)
3.重载原理:编译器会将提取关键信息,表面上函数名不变,实际上函数重新命名,从而实现调用。函数重载属于静态联编,其函数具体的调用是发生在编译阶段
4.函数重载实现的基本原理:
- 在编译时,编译器提取函数核心信息(函数名,行参列表)
- 然后对函数名进行修饰,生成一个惟一的内部修饰符
- 如果我们代码中调用的函数,编译器会根据实参的类型,顺序,个数去匹配修饰符
- 最终在链接阶段,通过修饰符找到正确的函数实现,从而避免调用混淆
cpp
#include <iostream>
// 1. 重载函数:参数个数不同
void display(int a) {
std::cout << "调用 display(int a),值为: " << a << std::endl;
}
void display(int a, int b) {
std::cout << "调用 display(int a, int b),值为: " << a << " 和 " << b << std::endl;
}
// 2. 重载函数:参数类型不同
void printValue(int i) {
std::cout << "调用 printValue(int),整型值为: " << i << std::endl;
}
void printValue(double d) {
std::cout << "调用 printValue(double),浮点值为: " << d << std::endl;
}
// 3. 重载函数:参数顺序不同
void show(int x, char y) {
std::cout << "调用 show(int, char),值为: " << x << " 和 " << y << std::endl;
}
void show(char y, int x) {
std::cout << "调用 show(char, int),值为: " << y << " 和 " << x << std::endl;
}
int main() {
// 参数个数不同
std::cout << "--- 参数个数不同 ---" << std::endl;
display(10); // 匹配 display(int)
display(20, 30); // 匹配 display(int, int)
// 参数类型不同
std::cout << "\n--- 参数类型不同 ---" << std::endl;
printValue(100); // 匹配 printValue(int)
printValue(3.14); // 匹配 printValue(double)
// 参数顺序不同
std::cout << "\n--- 参数顺序不同 ---" << std::endl;
show(5, 'A'); // 匹配 show(int, char)
show('B', 6); // 匹配 show(char, int)
return 0;
}
运行结果:
形参默认值:
- 可以给某个行参添加默认值
- 应用场景:参数重复且固定
- 当函数中针对具有默认值的行参,在实参未书写时使用行参的默认值
- 如果实参进行的具体数据的传递,则使用实参的值。
形参默认值规则:
- 如果函数有声明有定义,那么行参默认值通常出现在声明中。(行参默认值不可以都出现在声明和定义中)
- 如果函数没有声明只有定义,那么行参默认值就可以出现在定义处
- 一个函数可以拥有多个带默认值的行参
- 如果行参没有默认值,那么调用函数时,实参不可被省略。
- 如果行参列表中存在具有默认值的行参时,拥有默认值的行参必须处于行参列表后端(对于fun(),当其中行参a有默认值,后面的b和c都要有默认值)
- 拥有行参默认值的函数,切忌在遇到函数重载时,不要出现二义性(选择性)。例如有两个fun(),第一个void fun(int a,int b,int c = 10),第二个void fun(int a,int b),在main函数调用时fun(10,20),由于第一个fun中第三个参数有默认值,系统并不知道调用哪个fun函数
示例程序:
cpp
#include <iostream>
using namespace std;
void fun(int a,int b,int c = 10);
void fun(int a,int b,int c)
{
int sum = (a+b) * c /10;
cout << "a = " << a << endl;
cout << "b = " << b << endl;
cout << "c = " << c << endl;
cout << "sum = " << sum << endl;
}
int main(int argc,char *argv[])
{
fun(100,200);
return 0;
}
运行结果:
二,构造函数
1.构造函数:
构造函数是一种特殊的类成员函数,它在创建类的新对象(实例)时被自动调用。它的主要职责是初始化对象,确保对象在创建后处于一个有效的、可用的状态。
2.构造函数核心特性:
1)基本语法与定义
-
- 构造函数的函数名必须与类名一致。 这是硬性规定,编译器通过这一点来识别构造函数。
-
- 构造函数没有返回值类型。 这包括 void 也不可以写。它的隐式"返回"就是它所创建和初始化的那个对象。
-
- 如果没有显式声明构造函数,系统会为类自动生成一个默认构造函数。 这个默认构造函数是无参的,函数体为空,它会对类的成员变量进行默认初始化。
-
- 如果显式声明了任何一个构造函数(无论有无参数),系统将不再自动生成默认构造函数。 这一点非常重要。如果你定义了一个需要参数的构造函数,那么你就不能再以无参的方式创建对象了,除非你再显式地提供一个无参的构造函数。
2)调用时机与方式
- 1.在实例化对象时,构造函数会被自动调用。 你不需要像普通函数那样手动调用它。
- 2.构造函数不允许显式调用。 这意味着你不能像 myObject.Constructor() 这样去调用它。它的调用是和对象的创建过程绑定的。
- 3.new 会触发构造函数,而 malloc 不会。 new 在 C++ 中做了两件事:① 分配内存;② 调用构造函数进行初始化。而 malloc 是 C 语言的库函数,它只负责分配内存,不会调用构造函数。因此,在 C++ 中创建对象(尤其是动态创建)必须使用 new。
3)主要功能与作用
- 1.构造函数主要用于进行类的初始化操作。 这是它的核心使命,例如为成员变量赋予初始值。
- 2.对象实例化时,成员变量先被初始化(分配内存),然后构造函数的函数体才会被执行。 更准确地说,初始化的顺序是:先按照成员列表的顺序初始化成员变量,然后再执行构造函数 {} 中的代码。推荐使用"初始化列表"来初始化成员。
-
- 构造函数的形参通常是用于初始化类成员变量。 通过传递参数,我们可以创建出状态各不相同的对象。
4)高级特性
- 1.构造函数也可以进行重载,规则和函数重载一样。 一个类可以有多个构造函数,只要它们的参数列表(参数的个数、类型或顺序)不同即可。
- 2.当实例化一个类对象时,类中的多个构造函数只会有一个符合要求的被调用执行。 编译器会根据你创建对象时提供的实参来匹配最合适的构造函数版本。
- 3.构造函数的形参也可以设置默认值。 这使得构造函数的调用更加灵活。
- 4.如果构造函数被设置为 private 属性,那么在类的外部无法实例化类对象。 这种技术常用于实现某些设计模式,如 单例模式 (Singleton Pattern),即一个类只允许创建一个实例。
关于缺省的构造函数 ,说明以下几点:
1、在定义类时,只要显式定义了一个类的构造函数,则编译器就不产生缺省的构造函数
2、所有的对象在定义时,必须调用构造函数
3、在类中,若定义了没有参数的构造函数,或各参数均有缺省值的构造函数也称为缺省的构造函数,缺省的构造函数只能有一个,也就是说如果提供了一个无参构造函数,就不能同时存在参数均有缺省值的构造函数。
4、产生对象时,系统必定要调用构造函数。所以任一对象的构造函数必须唯一。
不存在没有构造函数的对象!
简单样例代码:
cpp
#include <iostream>
using namespace std;
class Node
{
public:
Node();
void display();
private:
int m_A;
int m_B;
float m_C;
};
Node::Node()
{
m_A = 10;
m_B = 11;
m_C = 12;
cout << "construct" << endl;
}
void Node::display()
{
cout << "m_A = " << m_A << endl;
cout << "m_B = " << m_B << endl;
cout << "m_C = " << m_C << endl;
}
int main()
{
Node n;
n.display();
return 0;
}
运行结果:
复杂样例代码:
cpp
#include <iostream>
#include <string>
class Student {
private:
std::string name;
int age;
int id;
public:
// 1. 无参构造函数
// 如果定义了下面的有参构造函数,就必须也定义一个无参的,否则 Student s1; 会编译失败。
Student() {
name = "未知";
age = 0;
id = -1;
std::cout << "无参构造函数被调用!" << std::endl;
}
// 2. 有参构造函数 (重载)
Student(std::string n, int a, int studentId) {
name = n;
age = a;
id = studentId;
std::cout << "有参构造函数被调用,为 " << name << " 初始化!" << std::endl;
}
// 3. 带默认参数的构造函数 (重载)
Student(int studentId) {
name = "新生";
age = 18;
id = studentId;
std::cout << "带默认参数的构造函数被调用,为 ID:" << id << " 初始化!" << std::endl;
}
void display() {
std::cout << "姓名: " << name << ", 年龄: " << age << ", 学号: " << id << std::endl;
}
};
int main() {
// 实例化对象时自动调用构造函数
std::cout << "--- 栈上创建对象 ---" << std::endl;
Student s1; // 自动调用无参构造函数
s1.display();
Student s2("张三", 20, 1001); // 根据实参,自动调用有参构造函数
s2.display();
Student s3(1002); // 自动调用带默认参数的构造函数
s3.display();
std::cout << "\n--- 堆上创建对象 (使用new) ---" << std::endl;
// 使用 new 会触发构造函数的调用
Student* p_s4 = new Student("李四", 21, 1003);
p_s4->display();
// 清理堆内存
delete p_s4;
return 0;
}
运行结果:
三,析构函数
1.析构函数:
析构函数是类的成员函数之一,它的名字与类名相同,但在前面加上一个波浪号(~)。它在对象的生命周期结束时被自动调用。其主要目的是释放对象在生命周期内分配的资源,并执行一些清理工作,从而防止资源泄漏。
2.析构函数核心特性:
- 析构函数没有返回类型,且析构函数没有形参。
- 析构函数不存在重载。
- 析构函数的函数名与类名相同,但是需要再函数名之前加 ~ 符号。语法: ~类名();
- 析构函数会在类对象生命周期结束时被自动调用。
- 当使用delete去释放一个类指针时,也会触发析构函数的调用。
- 通常只要出现类声明我们就需要显示书写析构函数,哪怕当前析构什么事也不干。
- 析构函数如果没有显示声明,系统会生成一个默认的析构函数。格式为 : ClassName::~ClassName() { };
- free不会触发析构函数的调用。
- 析构函数不可以被设置为private。
- 析构函数可以通过类对象手动调用,但是不允许这样操作。
- 构造函数的调用顺序,与析构函数的调用顺序是相反的。(只针对栈上,如果是类的指针,先delete谁,就先析构(释放)谁)
- 如果在构造函数中使用了new或malloc等动态开辟内存,那么默认的析构函数中是不会进行内存释放的,需要我们显示书写析构,然后手动调用delete或free进行释放。
综合示例代码:
cpp
#include <iostream>
#include <string>
#include <cstdlib>
#include <new>
// 基类,用于演示构造/析构顺序
class Base {
public:
Base() {
std::cout << " [Base] 构造函数被调用。" << std::endl;
}
// 6 & 7: 即使基类不做任何事,也最好提供一个虚析构函数
// 1, 2, 3: 无返回、无参数、~类名 语法
virtual ~Base() {
std::cout << " [Base] 析构函数被调用。" << std::endl;
}
};
class ResourceHolder : public Base {
private:
std::string name;
int* data;
public:
// 构造函数
ResourceHolder(const std::string& n) : name(n) {
std::cout << "构造函数: 为对象 '" << name << "' 分配资源..." << std::endl;
// 12: 动态开辟内存
data = new int[5];
}
// 1、析构函数没有返回类型,且析构函数没有形参。
// 2、析构函数不存在重载。(下面的注释代码会引发编译错误)
// 3、析构函数的函数名与类名相同,但是需要再函数名之前加 ~ 符号。
~ResourceHolder() {
std::cout << "析构函数: 正在为对象 '" << name << "' 释放资源..." << std::endl;
// 12: 手动释放构造函数中动态开辟的内存
delete[] data;
data = NULL; // 良好的编程习惯
}
// 错误:析构函数不能重载
// ~ResourceHolder(int mode) {}
void Greet() {
std::cout << "对象 '" << name << "' 向你问好!" << std::endl;
}
};
void demonstrate_stack_object() {
std::cout << "\n--- 演示栈对象 (自动调用析构) ---" << std::endl;
// 4: 对象 obj_stack 在其作用域(这个函数)结束时,生命周期结束,析构函数被自动调用。
ResourceHolder obj_stack("StackObject");
obj_stack.Greet();
std::cout << "即将离开 demonstrate_stack_object 函数作用域..." << std::endl;
}
void demonstrate_constructor_destructor_order() {
std::cout << "\n--- 演示构造与析构的相反顺序 ---" << std::endl;
ResourceHolder first("First");
ResourceHolder second("Second");
// 11: 构造顺序是 first -> second。当函数结束时,析构顺序是 second -> first (后进先出)。
std::cout << "即将离开 demonstrate_constructor_destructor_order 函数作用域..." << std::endl;
}
int main() {
// 演示第5点: 使用 new 和 delete
std::cout << "--- 演示堆对象 (delete触发析构) ---" << std::endl;
ResourceHolder* obj_heap = new ResourceHolder("HeapObject");
obj_heap->Greet();
// 5: 当使用delete去释放一个类指针时,会先触发析构函数,然后再释放内存。
delete obj_heap;
obj_heap = NULL;
// 演示第4点
demonstrate_stack_object();
// 演示第11点
demonstrate_constructor_destructor_order();
// 演示第8点: free 不会触发析构
std::cout << "\n--- 演示 malloc/free (析构函数不会被调用) ---" << std::endl;
void* memory = malloc(sizeof(ResourceHolder)); // 只分配了原始内存
ResourceHolder* obj_malloc = new(memory) ResourceHolder("MallocObject"); // 使用 placement new 构造对象
obj_malloc->Greet();
// 8: free 只会释放内存,它不知道这块内存上还有一个C++对象,因此不会调用析构函数!
// 这会导致内存泄漏,因为 ResourceHolder 内部的 data 指针没有被 delete[]。
free(obj_malloc);
std::cout << "调用 free 之后,你不会看到 'MallocObject' 的析构函数输出。" << std::endl;
// 演示第10点: 手动调用析构函数(不推荐使用)
std::cout << "\n--- 演示手动调用析构 ---" << std::endl;
ResourceHolder obj_manual("ManualCallObject");
obj_manual.Greet();
// 10: 可以通过对象手动调用析构函数,但这通常会导致问题。
// 因为当 obj_manual 离开作用域时,析构函数会被再次自动调用,导致重复释放内存,引发未定义行为。
std::cout << "准备手动调用析构函数..." << std::endl;
obj_manual.~ResourceHolder();
std::cout << "手动调用完成。注意:当main函数结束时,析构函数会被再次调用!" << std::endl;
return 0;
}
四,构造函数与new
可以使用new运算符来动态地建立对象。建立时要自动调用构造函数,以便完成初始化对象的数据成员。最后返回这个动态对象的起始地址。
用new运算符产生的动态对象,在不再使用这种对象时,必须用delete运算符来释放对象所占用的存储空间。
用new建立类的对象时,可以使用参数初始化动态空间。
可以用new运算符为对象分配存储空间,如:
A *p;
p=new A;
这时必须用delete才能释放这一空间。
delete p;
用new运算符为对象分配动态存储空间时,调用了构造函数,用delete删除这个空间时,调用了析构函数。当使用运算符delete删除一个由new动态产生的对象时,它首先调用该对象的析构函数,然后再释放这个对象占用的内存空间。
示例程序:
cpp
#include <iostream>
#include <string>
class Gadget {
private:
std::string name_;
int id_;
public:
// 构造函数:接收参数以初始化数据成员
Gadget(const std::string& name, int id) : name_(name), id_(id) {
std::cout << " [构造函数] Gadget '" << name_ << "' (ID: " << id_
<< ") 已被创建并初始化。" << std::endl;
}
// 析构函数:在对象销毁前执行清理工作
~Gadget() {
std::cout << " [析构函数] Gadget '" << name_ << "' (ID: " << id_
<< ") 即将被销毁,资源已清理。" << std::endl;
}
// 一个普通的成员函数,用来显示对象信息
void ShowInfo() {
std::cout << " -> 信息: 这是一个 '" << name_ << "', ID是 " << id_ << "." << std::endl;
}
};
int main() {
std::cout << "--- 程序开始 ---" << std::endl;
// 1. 声明一个指向 Gadget 类型的指针
Gadget* gadget_ptr = NULL;
std::cout << "1. 指针 gadget_ptr 已声明。" << std::endl;
// 2. 使用 new 动态创建对象,并传入参数调用构造函数
std::cout << "\n2. 准备使用 new 创建动态对象..." << std::endl;
gadget_ptr = new Gadget("智能手表", 101);
std::cout << " 对象已创建,地址为: " << gadget_ptr << std::endl;
// 3. 检查对象是否创建成功,并使用它
if (gadget_ptr != NULL) {
std::cout << "\n3. 使用动态创建的对象..." << std::endl;
gadget_ptr->ShowInfo(); // 通过指针调用成员函数
}
// 4. 使用 delete 销毁对象并释放内存
std::cout << "\n4. 准备使用 delete 销毁对象..." << std::endl;
delete gadget_ptr;
std::cout << " 对象已销毁,内存已释放。" << std::endl;
// 5. 将指针置空,防止成为野指针(悬垂指针)
gadget_ptr = NULL;
std::cout << "\n5. 指针 gadget_ptr 已被置为 nullptr,操作安全结束。" << std::endl;
std::cout << "\n--- 程序结束 ---" << std::endl;
return 0;
}
代码执行流程:
- Gadget* gadget_ptr = nullptr;
这里声明了一个指针 gadget_ptr,它未来可以指向一个 Gadget 类型的对象。初始化为NULL表示它当前不指向任何对象。
- gadget_ptr = new Gadget("智能手表", 101);
new 运算符被调用。
它首先在堆上分配足够容纳一个 Gadget 对象的内存空间。
接着,自动调用 Gadget 类的构造函数,并将参数 "智能手表" 和 101 传递给它。此时,会看到构造函数的输出信息。
构造函数执行完毕后,对象初始化完成。
最后,new 运算符返回这个新创建对象的内存地址,该地址被赋值给 gadget_ptr 指针。
- gadget_ptr->ShowInfo();
通过指针和箭头运算符 -> 来调用动态对象的成员函数,证明这个对象确实存在并且已经被正确初始化了。
- delete gadget_ptr;
这是整个生命周期管理的关键一步。当执行 delete 时,会发生两件事,顺序是固定的:
第一步:调用析构函数。 系统会首先自动调用 gadget_ptr 指向的对象的析构函数 ~Gadget()。此时,会看到析构函数的输出信息。这是执行任何必要清理工作的最后机会。
第二步:释放内存。 在析构函数执行完毕后,delete 运算符才会释放之前由 new 分配的内存空间,将其归还给操作系统。
- gadget_ptr = nullptr;
在 delete 之后,虽然内存被释放了,但 gadget_ptr 变量本身仍然存储着那个(现在已经无效的)内存地址。此时的指针被称为悬垂指针(dangling pointer)。如果之后不小心再次使用它,会导致未定义的行为(通常是程序崩溃)。将其设置为 nullptr 可以明确表示它不再指向任何有效对象。
运行结果:
不同存储类型的对象调用构造函数及析构函数:
1、对于全局定义的对象(在函数外定义的对象),在程序开始执行时,调用构造函数;到程序结束时,调用析构函数。
2、对于局部定义的对象(在函数内定义的对象),当程序执行到定义对象的地方时,调用构造函数;在退出对象的作用域时,调用析构函数。
3、用static定义的局部对象,在首次到达对象的定义时调用构造函数;到程序结束时,调用析构函数
4、对于用new运算符动态生成的对象,在产生对象时调用构造函数,只有使用delete运算符来释放对象时,才调用析构函数。若不使用delete来撤消动态生成的对象,程序结束时,对象仍存在,并占用相应的存储空间,即系统不能自动地调用析构函数来撤消动态生成的对象。
实现类型转换的构造函数(隐式构造)
同类型的对象可以相互赋值,相当于类中的数据成员相互赋值; 如果直接将数据赋给对象,所赋入的数据需要类型转换为一个对象,这种转换需要调用构造函数。转换过程叫隐式构造。
注意:当构造函数只有一个参数时(或只需要传递一个参数),构造对象时可以用= 直接赋值。
示例代码:
cpp
#include <iostream>
class Counter {
private:
int value_;
public:
// 这是一个类型转换构造函数
// 它可以只用一个参数 (int) 来调用
Counter(int value) : value_(value) {
std::cout << " -> 调用了 Counter 的构造函数 (int value = " << value_
<< ")。一个 int 被转换为了 Counter 对象。" << std::endl;
}
// 为了演示,提供一个默认构造函数
Counter() : value_(0) {
std::cout << " -> 调用了 Counter 的默认构造函数。" << std::endl;
}
void Display() const {
std::cout << " Counter 的值是: " << value_ << std::endl;
}
};
// 这个函数需要一个 Counter 类型的对象作为参数
void PrintCounter(Counter c) {
std::cout << "在 PrintCounter 函数内部:" << std::endl;
c.Display();
}
int main() {
std::cout << "--- 1. 标准的构造方式 ---" << std::endl;
Counter c1(10); // 直接调用构造函数
c1.Display();
std::cout << "\n--- 2. 隐式构造 (初始化时) ---" << std::endl;
// 注意:当构造函数只有一个参数时(或只需要传递一个参数),构造对象时可以用= 直接赋值。
// 这行代码不是赋值!而是初始化。
// 编译器看到 int 类型的 20,发现需要一个 Counter 对象,
// 于是自动调用 Counter(20) 来创建一个临时的 Counter 对象,然后用它来初始化 c2。
Counter c2 = 20;
c2.Display();
std::cout << "\n--- 3. 隐式构造 (函数传参时) ---" << std::endl;
// 调用 PrintCounter,但传递的是一个 int (30),而不是一个 Counter 对象。
// 编译器会自动调用 Counter(30) 来创建一个临时的 Counter 对象,并将其作为参数传给函数。
PrintCounter(30);
std::cout << "\n--- 4. 赋值操作 (与隐式构造的区别) ---" << std::endl;
Counter c3; // 调用默认构造函数
std::cout << "准备执行赋值操作..." << std::endl;
// 这里是赋值,不是初始化。
// 编译器仍然会先调用 Counter(40) 创建一个临时对象。
// 然后,调用 Counter 类默认生成的赋值运算符函数,将临时对象的数据赋给 c3。
c3 = 40;
c3.Display();
std::cout << "\n--- 程序结束 ---" << std::endl;
return 0;
}
关键点解析:
1.核心规则:Counter(int value) 这个构造函数是整个机制的关键。因为它能用一个 int 参数来调用,所以编译器就学会了如何把一个 int 变成一个 Counter。
2.初始化 vs. 赋值:
Counter c2 = 20; 看起来像赋值,但因为它是在对象声明的同时进行的,所以这在 C++ 中是初始化(Initialization)。它和 Counter c2(20); 在语义上是等价的,都会触发隐式构造。
c3 = 40; 是在 c3 已经被创建之后执行的,所以这是赋值(Assignment)。这个过程稍微复杂:首先通过 Counter(40) 创建一个匿名的临时对象,然后通过 Counter 的赋值运算符(这里是编译器默认生成的)将这个临时对象的值拷贝给 c3。
3.函数参数:PrintCounter(30); 是隐式转换最常见的应用场景之一。它极大地简化了代码,使得可以像传递基本数据类型一样传递一个"概念上"的对象,而无需手动创建它。
运行结果: