C++核心编程—(面向对象,引用,函数提高,内存分区模型)

1. 内存分区模型

1.1 内存模型

C++程序在执行时,将内存大方向划分为4个区域

  • 代码区:存放函数体的二进制代码,由操作系统进行管理的(编译后的exe文件)

  • 全局区静态:存放全局变量和静态变量以及常量

  • 栈区:由编译器自动分配释放, 存放函数的参数值,局部变量等

  • 堆区:由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收

1.2 程序运行前后

在程序编译后,生成了exe可执行程序,未执行该程序前分为两个区域

代码区:

存放 CPU 执行的机器指令

代码区是共享的,共享的目的是对于频繁被执行的程序,只需要在内存中有一份代码即可

代码区是只读的,使其只读的原因是防止程序意外地修改了它的指令

全局区静态区:

全局变量静态变量存放在此.

全局区还包含了常量区, 字符串常量和其他常量也存放在此.

==该区域的数据在程序结束后由操作系统释放==.

1.3 程序运行后

栈区:

由编译器自动分配释放, 存放函数的参数值,局部变量等

注意事项:不要返回局部变量的地址,栈区开辟的数据由编译器自动释

堆区:

由程序员分配释放,若程序员不释放,程序结束时由操作系统回收

在C++中主要利用new在堆区开辟内(c中使用mallc)

总结:

堆区数据由程序员管理开辟和释放

堆区数据利用new关键字进行开辟内

1.4 new关键字

C++中利用==new==操作符在堆区开辟数据

堆区开辟的数据,由程序员手动开辟,手动释放,释放利用操作符 ==delete==

语法:new 数据类型

利用new创建的数据,会返回该数据对应的类型的指

代码示例1:

cpp 复制代码
#include <iostream>
using namespace std;

// 全局变量,存储在全局区
int g_var = 10;

// 静态变量,存储在全局区
static int s_var = 20;

// 字符串常量,存储在全局区
const char *str = "Hello, world!";

// 常量变量,存储在全局区
const int c_var = 30;

// 函数,存储在代码区
void func() {
// 局部变量,存储在栈区
int a = 10;

// 动态分配内存,存储在堆区
int *p = new int;
*p = 20;

cout << "a: " << a << endl;
cout << "*p: " << *p << endl;

// 释放动态分配的内存
delete p;
}

int main() {
// 调用函数
func();
system("pause");
return 0;
}

代码示例2:

cpp 复制代码
//堆区开辟数组
int main() {

	int* arr = new int[10];

	for (int i = 0; i < 10; i++)
	{
		arr[i] = i + 100;
	}

	for (int i = 0; i < 10; i++)
	{
		cout << arr[i] << endl;
	}
	//释放数组 delete 后加 []
	delete[] arr;

	system("pause");

	return 0;
}

2. 引用

想象一下你有一个变量 int score = 95;,它存储在内存的某个地方(比如地址 0x1234)。引用和指针都提供了一种方式,让你不直接通过变量名 score,而是通过另一个"中介"来访问或修改这个 95。这个"中介"本身存储着变量 score内存地址

2.1 指针(详述)

C语言进阶---函数(static,递归,回调,指针,内联,变参,结构体尺寸)-CSDN博客

2.2 引用(详述)

1. 定义

  • 引用是一个已存在变量的别名(另一个名字)

  • 不是 一个新的变量,不占用额外的存储空间(在底层实现上,编译器通常还是用指针来实现引用,但这是编译器的事,从语言层面看,它就是一个别名)。

  • 想象一下,你有一个朋友叫"李雷",你给他起了个外号叫"大雷"。无论你用"李雷"还是"大雷"叫他,指的都是同一个人。引用就是变量的"外号"。

  • 引用必须在声明时初始化 ,并且一旦绑定到一个变量,在其整个生命周期内就不能再绑定到其他变量。它和它的目标"同生共死"。

变量就是张三,三哥就是张三的引用,这个三哥与张三建立联系之后,三哥就是张三的属性了,不可更改,张三销毁的时候,三哥也销毁了。

2. 初始化

  1. 声明与初始化:

    • 使用 & 符号声明(注意:这里的 & 是引用声明符,不是取地址符)。

    • 语法:数据类型 &引用名 = 目标变量名; (初始化是声明的一部分,必须立刻完成!)

    • 示例:

      cpp 复制代码
      //声明变量
      int count = 10;
      int &refToCount = count; // refToCount 是 count 的引用(别名)
      
      double pi = 3.14159;
      double &refToPi = pi;    // refToPi 是 pi 的引用

3. 使用

  • 使用引用就像使用原始变量一样。不需要任何解引用操作符(如 *)。

  • 对引用的所有操作直接作用在它所引用的原始变量上

  • 示例:

    cpp 复制代码
    #include <iostream>
    using namespace std;
    
    int main()
    {
    
        int score = 95;
        int &refScore = score; // refScore 是 score 的别名
    
        cout << score << endl;    // 输出 95
        cout << refScore << endl; // 输出 95 (直接使用,像变量一样)
    
        refScore = 100;           // 通过引用修改值 (实际修改的是 score)
        cout << score << endl;    // 输出 100
        cout << refScore << endl; // 输出 100
    
        // 试图改变 refScore 指向另一个变量?不行!
        int anotherScore = 60;
        // refScore = anotherScore; // 错误!这不是改变引用指向,这是赋值操作。
        // 上面这行代码的意思是把 anotherScore 的值 (60) 赋给 refScore 所引用的变量(也就是 score)。结果:
        cout << score << endl;        // 输出 60
        cout << anotherScore << endl; // 输出 60
                                      // refScore 仍然绑定的是 score,而不是 anotherScore。
    
        return 0;
    }

4. 特性:

  • 必须初始化: 声明时必须绑定到一个已存在的变量。

  • 不可重新绑定: 一旦初始化绑定到一个变量,就不能再改变去绑定另一个变量。

  • 无空引用: 引用必须总是指向一个有效的对象。不存在"空引用"。(尝试创建空引用是未定义行为)。

  • 无需解引用: 使用引用就像使用原始变量名一样自然直接。

  • 本质是别名: 它不是一个独立的实体,只是已有对象的一个新名字。

2.3 指针与引用核心总结

特性 引用 (Reference) 指针 (Pointer)
本质 别名 (已存在变量的另一个名字) 变量 (存储另一个变量的内存地址)
声明 int &ref = var; (必须初始化) int *ptr;int *ptr = &var; (可后初始化)
空值 不允许。必须绑定有效对象。 允许 。可以设为 nullptr
重新绑定 不允许。一旦初始化绑定,终身绑定该对象。 允许。可以指向不同对象(同类型或兼容类型)。
访问目标值 直接 使用引用名 (ref = 10;) 需要解引用 (*ptr = 10;)
内存占用 不额外占用存储空间(概念层面)。 占用存储空间(存储地址值,通常4/8字节)。
安全性 相对安全(无空引用、无野引用的情况下)。 风险较高(可能空指针、野指针、内存泄漏)。
运算符 声明用 &(引用符),使用无特殊符。 声明用 *(指针符),取地址用 &,解引用用 *
参数传递 常用于按引用传递(修改实参、避免大对象拷贝)。 也可用于按地址传递(修改实参、需要显式处理地址)。
指针运算 不支持 (如 ++ref 是目标值+1,不是地址+1) 支持 (如 ++ptr 移动到下一个元素地址)。
多级间接 不支持 引用链(如 int && 是右值引用)。 支持 多级指针(如 int **pp)。

野指针 (Dangling Pointer) :指向已经被释放( delete)或已经离开作用域的内存区域的指针。访问野指针是危险的未定义行为。引用理论上也可能成为"野引用"(例如,引用一个局部变量,但该变量所在的函数已返回),但通常编译器会警告或更容易避免。指针更容易意外成为野指针。

2.4 为何要使用指针与引用,以及其使用场景

1. 函数参数传递---修改实参

1.1 指针

引用: 最常见、最简洁的方式。函数内部修改形参(引用),直接影响外部的实参。调用语法和传值一样。

cpp 复制代码
#include <iostream>
using namespace std;

int test1(int *a)
{
    int local_a = *a;
    return local_a;
}

int main()
{
    int c = 10;
    int b = test1(&c);
    printf("%d\n", b);
    return 0;
}
1.2 引用

引用: 最常见、最简洁的方式。函数内部修改形参(引用),直接影响外部的实参。调用语法和传值一样。

cpp 复制代码
#include <iostream>
using namespace std;

// 引用传递的本质是指针传递,但是引用传递的代码更加简洁
//参数引用传递使用const是因为const修饰的变量不能被修改,防止在函数中修改参数的值,从而影响函数的调用者
// 引用传递的效率比指针传递的效率高,因为引用传递不需要进行指针的解引用操作,直接使用变量的值即可
int test1(const int &a) 
{
    int local_a = a;
    return local_a;
}

int main()
{
    int c = 10;
    int w = test1(c);
    printf("w = %d\n", w);
    return 0;
}

2. 函数参数传递---避免大对象拷贝

  • 传递大型结构体(struct)或类对象(class)时,传值会导致整个对象被复制一份,开销大。

  • 常量引用 (const &): 最佳实践。既避免了拷贝,又保证函数内部不会意外修改原始对象(const 保护)。

  • 指针也可以避免拷贝(const VeryLargeObject *ptr),但访问成员需要使用 ->(*ptr).member,不如引用直接 . 方便。

3. 实现链式操作

  • 引用常用在运算符重载(如赋值运算符 =)或某些成员函数中返回 *this 的引用,以实现 obj.func1().func2().func3() 这样的链式调用。

4. 动态内存管理

  • 指针是必须的! new 操作符返回的是指向新分配内存的指针 。你需要用指针来访问和释放(delete)这块内存。

    cpp 复制代码
    int *dynamicArray = new int[100]; // 动态分配100个整数的数组
    dynamicArray[0] = 42;             // 通过指针访问
    delete[] dynamicArray;            // 必须用指针来释放内存
  • 引用不能 用于直接管理动态内存(new 不返回引用)。虽然你可以让一个引用指向动态分配的对象,但释放仍然需要通过指针(或智能指针)。

5. 可选参数/灵活数据结构

  • 指针允许 nullptr,可以用来表示"无数据"或"可选参数"。引用没有空值,不适用这种场景。

  • 指针是实现链表、树、图等复杂数据结构节点的基石(节点包含指向下一个/子节点的指针)。

6. 与c库交互

  • 很多C语言库的API使用指针参数。在C++中调用这些API时,通常需要传递指针(或指向指针的指针)。

2.5 初学者建议

  1. 优先考虑引用: 当你需要在函数内部修改实参、避免大对象拷贝时,首选引用 (特别是 const &)。它语法更简洁安全。

  2. 必须用指针时再用指针: 当你需要动态内存分配(new/delete)、需要表示"可能为空"的状态、需要构建复杂的数据结构(链表、树)、或者需要与C接口交互时,使用指针

  3. 初始化!初始化!初始化! 指针在定义后尽快初始化(指向有效对象或 nullptr)。引用则必须在定义时初始化。

  4. 警惕空指针: 在使用指针之前(特别是函数参数),养成习惯检查它是否为 nullptr(如果它允许为空的话)。

  5. 避免野指针: 确保指针指向的内存是有效的。在 delete 一个指针后,立即将其置为 nullptr。不要返回指向局部变量的指针或引用(局部变量离开函数就销毁了)。

  6. 理解 &* 在不同上下文中的含义:

    • 声明中:

      • int *p; -> * 表示 p 是一个指针。

      • int &r = x; -> & 表示 r 是一个引用。

    • 表达式中:

      • p = &x; -> & 是取地址运算符,获取 x 的地址。

      • y = *p; -> * 是解引用运算符,获取 p 指向地址的值。

3. 函数提高

在C++中,函数的形参列表中的形参是可以有默认值的。

语法:返回值类型 函数名 (参数= 默认值){

3.1 函数默认参数

1. 是什么?

函数默认参数允许你在声明函数时为参数指定默认值。当调用函数时没有提供该参数的值,就会自动使用这个默认值。

2. 为什么需要?
  1. 提高函数灵活性:调用者可以省略某些参数

  2. 简化函数调用:避免为常见情况重复输入相同值

  3. 向后兼容:添加新参数时不影响旧代码

3. 规则:
  1. 默认参数必须从右向左连续设置

  2. 默认值只能指定一次(通常在函数声明中)

  3. 默认值可以是常量、全局变量或函数调用

  4. 不能同时使用默认参数和函数重载造成歧义

示例:

cpp 复制代码
#include <iostream>
using namespace std;

// 函数声明(指定默认参数)
void printInfo(string name, int age = 18, string city = "北京");

int main() {
    // 不同调用方式
    printInfo("张三");           // 使用两个默认参数
    printInfo("李四", 25);        // 使用一个默认参数
    printInfo("王五", 30, "上海"); // 不使用默认参数
    
    return 0;
}

// 函数定义(不再重复指定默认值)
void printInfo(string name, int age, string city) {
    cout << "姓名:" << name 
         << ",年龄:" << age
         << ",城市:" << city << endl;
}

输出:

bash 复制代码
姓名:张三,年龄:18,城市:北京
姓名:李四,年龄:25,城市:北京
姓名:王五,年龄:30,城市:上海

错误示例:

cpp 复制代码
// 错误1:非连续默认参数
void func(int a = 1, int b);  // 编译错误:b没有默认值

// 错误2:重复指定默认值
void func(int a = 1);
void func(int a = 1) { ... }  // 重定义默认参数

3.2 函数占位参数

1. 是什么?

占位参数是只有类型声明而没有参数名的函数参数 。调用时必须传入对应类型的值 ,但在函数内部无法使用该参数

2. 为什么需要?
  1. 预留未来扩展:为后续功能保留参数位置

  2. 保持函数签名:兼容特定接口要求

  3. 运算符重载:某些特殊场景下的语法要求

  4. 区分函数重载:作为参数列表不同的标识

3. 规则:
  1. 只有类型声明,没有参数名

  2. 调用时必须传递对应类型的实参

  3. 通常用int类型(也可用其他类型)

  4. 可以结合默认参数使用

示例:

cpp 复制代码
#include <iostream>
using namespace std;

// 占位参数函数声明
void placeFunc(int a, int, double);  // 第二个参数是占位

int main() {
    placeFunc(10, 20, 3.14);  // 必须传入三个参数
    return 0;
}

// 函数定义(占位参数仍需要声明)
void placeFunc(int a, int, double c) {
    cout << "a = " << a << ", c = " << c << endl;
    // 注意:无法使用第二个参数(没有名字)
}
4. 结合默认参数:
cpp 复制代码
void placeholder(int a, int = 0);  // 占位参数带默认值

int main() {
    placeholder(5);     // 合法调用
    placeholder(5, 10); // 合法调用
}
5. 实际应用场景:
cpp 复制代码
// 1. 回调函数占位
void setCallback(void (*func)(int, int)) { /*...*/ }

// 2. 区分重载函数
void print(int a) { /*...*/ }
void print(int a, int) { /*...*/ }  // 占位参数实现重载

// 3. 兼容旧接口
void legacyAPI(int, char*);  // 保持参数数量不变

3.3 函数重载

1. 是什么?

函数重载允许在同一作用域内创建多个同名函数,只要它们的参数列表不同(参数类型、数量或顺序不同)。

2. 为什么需要?
  1. 提高可读性:相同功能使用统一名称

  2. 简化接口:根据参数自动选择实现

  3. 类型安全:避免不安全的类型转换

  4. 扩展功能:为不同类型提供相同操作

3. 核心原则:
  1. 函数名必须相同

  2. 参数列表必须不同(满足以下任一):

    • 参数数量不同

    • 参数类型不同

    • 参数顺序不同(需类型不同)

  3. 返回类型不能作为重载依据

示例解析:
cpp 复制代码
#include <iostream>
using namespace std;

// 1. 参数数量不同
void print(int a) {
    cout << "整数:" << a << endl;
}

void print(int a, int b) {
    cout << "两个整数:" << a << ", " << b << endl;
}

// 2. 参数类型不同
void print(double d) {
    cout << "浮点数:" << d << endl;
}

void print(string s) {
    cout << "字符串:" << s << endl;
}

// 3. 参数顺序不同
void print(int a, double d) {
    cout << "int+double:" << a << ", " << d << endl;
}

void print(double d, int a) {
    cout << "double+int:" << d << ", " << a << endl;
}

int main() {
    print(10);          // 调用 print(int)
    print(3.14);        // 调用 print(double)
    print("Hello");     // 调用 print(string)
    print(10, 20);      // 调用 print(int, int)
    print(5, 3.14);     // 调用 print(int, double)
    print(3.14, 5);     // 调用 print(double, int)
    
    return 0;
}
4. 重载解析规则:
  1. 精确匹配(类型完全相同)

  2. 提升转换(char→int,float→double)

  3. 标准转换(int→double,double→int)

  4. 用户定义转换(类转换构造函数)

5. 常见问题与陷阱:

1:返回类型不同不是重载
cpp 复制代码
int func(int a);         // 错误
double func(int a);      // 编译失败:仅返回类型不同
2:const修饰符的情况
cpp 复制代码
// 场景1:值传递 - const不构成重载
void func(int a);        // 重复声明
void func(const int a);  // 编译错误(顶层const)

// 场景2:指针/引用 - const构成重载
void func(int* p);       // 处理普通指针
void func(const int* p); // 处理常量指针(合法重载)

void func(string& s);       // 处理普通引用
void func(const string& s); // 处理常量引用(合法重载)
3:默认参数引起歧义
cpp 复制代码
void print(int a, int b = 10);  // 版本1
void print(int a);              // 版本2

print(5);  // 编译器困惑:该调用哪个版本?
           // 错误:歧义调用
4:类型转换歧义
cpp 复制代码
void calc(float a);
void calc(double b);

calc(3.14f);  // 调用float版本
calc(10);     // 歧义:int可转float或double
              // 错误:歧义调用

最佳实践:

  1. 确保重载函数功能语义相似

  2. 避免重载参数只有const不同的函数

  3. 谨慎结合默认参数和重载

  4. 使用显式类型转换解决歧义

  5. 对自定义类型提供重载运算符

实际应用案例:

cpp 复制代码
// 数学计算重载
int max(int a, int b);
double max(double a, double b);
float max(float a, float b);

// 字符串处理重载
string concat(const string& s1, const string& s2);
string concat(const char* s1, const string& s2);
string concat(const string& s1, const char* s2);

// 容器操作重载
class Vector {
public:
    void push_back(int value);
    void push_back(double value);
    void push_back(const string& value);
};

3.4 三者的协同工作

cpp 复制代码
// 默认参数 + 重载 + 占位参数
void process(int a);                          // 版本1
void process(double d, int placeholder = 0);  // 版本2(含默认和占位)

process(10);     // 调用版本1
process(3.14);   // 调用版本2(使用默认参数)
process(5.5, 1); // 调用版本2

1.总结对比表:

| 特性 | 默认参数 | 占位参数 | 函数重载 |
| 主要目的 | 简化调用,提供缺省值 | 保留参数位置,兼容接口 | 同名函数处理不同类型数据 |
| 参数要求 | 必须从右向左连续设置 | 只有类型没有名称 | 参数列表必须不同 |
| 调用时 | 可省略带默认值的参数 | 必须传入对应类型实参 | 编译器根据实参自动选择 |
| 函数内访问 | 正常使用参数 | 无法使用(无参数名) | 每个重载独立实现 |

典型应用场景 配置选项、可选参数 接口预留、运算符重载 数学计算、类型通用操作

4. 类和对象

对象编程的三大核心特性之一(封装、继承、多态)

4.1. 封装

4.1.1 封装的意义

什么是封装?

封装是将数据(属性)和操作数据的方法(函数)捆绑在一起形成一个"类"的过程,同时对外部隐藏内部实现细节,只暴露必要的接口。

封装三大核心意义:
  1. 数据保护

    • 防止外部代码随意修改对象内部状态

    • 避免非法数据导致对象状态不一致

    • 示例:银行账户的余额不能直接修改

  2. 实现隐藏

    • 隐藏类的内部实现细节

    • 外部只需知道"做什么",不需要知道"怎么做"

    • 示例:开车时只需操作方向盘、油门,不需要了解发动机工作原理

  3. 接口统一

    • 提供清晰、稳定的公共接口

    • 内部修改不影响外部使用

    • 示例:手机充电接口统一为USB-C,内部电路变化不影响使用

现实生活类比:

想象一个自动售货机:

  • 封装的数据:商品库存、金额计数

  • 封装的函数:投币、选择商品、出货

  • 隐藏的细节:内部机械结构、库存管理逻辑

  • 公共接口:投币口、选择按钮、取货口

代码示例:未封装 vs 封装

未封装的实现(问题明显):

cpp 复制代码
// 未封装的矩形类
class RawRectangle {
public:
    double width;
    double height;
};

int main() {
    RawRectangle rect;
    rect.width = 5.0;
    rect.height = -3.0; // 非法值!高度不能为负数
    
    double area = rect.width * rect.height; // 面积计算错误
    cout << "面积: " << area << endl; // 输出: 面积: -15
    
    return 0;
}

封装后的实现(安全可靠):

cpp 复制代码
// 封装的矩形类
class Rectangle {
private:
    double width;
    double height;

public:
    // 设置宽度(带验证)
    void setWidth(double w) {
        if (w > 0) {
            width = w;
        } else {
            cout << "错误:宽度必须大于0" << endl;
        }
    }
    
    // 设置高度(带验证)
    void setHeight(double h) {
        if (h > 0) {
            height = h;
        } else {
            cout << "错误:高度必须大于0" << endl;
        }
    }
    
    // 计算面积
    double getArea() const {
        return width * height;
    }
    
    // 获取宽度
    double getWidth() const { return width; }
    
    // 获取高度
    double getHeight() const { return height; }
};

int main() {
    Rectangle rect;
    rect.setWidth(5.0);
    rect.setHeight(-3.0); // 错误被捕获
    
    cout << "宽度: " << rect.getWidth() << endl; // 输出: 宽度: 5
    cout << "高度: " << rect.getHeight() << endl; // 输出: 高度: 0(默认值)
    cout << "面积: " << rect.getArea() << endl; // 输出: 面积: 0
    
    return 0;
}

4.1.2 struct和class区别

在C++中,structclass都用于定义类,但它们有重要区别:

核心区别:
特性 struct (结构体) class (类)
默认访问权限 public (公有) private (私有)
默认继承权限 public (公有继承) private (私有继承)
使用场景 主要用于数据聚合 用于完整对象实现
设计理念 C兼容性,简单数据结构 面向对象,封装
详细解释:
  1. 默认访问权限

    • struct成员默认是public

    • class成员默认是private

    cpp 复制代码
    struct PersonStruct {
        // 默认public
        string name; 
        int age;
    };
    
    class PersonClass {
        // 默认private
        string name;
        int age;
    };
  2. 默认继承权限

    • struct继承默认是public

    • class继承默认是private

    cpp 复制代码
    struct BaseStruct {};
    struct DerivedStruct : BaseStruct {}; // 默认public继承
    
    class BaseClass {};
    class DerivedClass : BaseClass {}; // 默认private继承
  3. 使用习惯

    • struct常用于纯数据结构

      cpp 复制代码
      struct Point {
          double x;
          double y;
      };
    • class用于需要封装行为的对象

      cpp 复制代码
      class BankAccount {
      private:
          double balance;
      public:
          void deposit(double amount);
          bool withdraw(double amount);
      };
      重要注意事项:
  • 技术上,两者功能几乎相同,区别仅在于默认设置

  • 可以显式改变默认行为:

    cpp 复制代码
    struct PrivateStruct {
    private: // 显式设置为私有
        int secret;
    };
    
    class PublicClass {
    public: // 显式设置为公有
        int openData;
    };
  • 在C++中优先使用class实现面向对象设计

  • 使用struct主要为了兼容C或表示简单数据结构

4.1.3 成员属性设置为私有

为什么要把成员属性设置为私有?
  1. 数据保护

    • 防止外部直接修改导致数据不一致

    • 示例:年龄不能为负数,余额不能直接修改

  2. 数据验证

    • 在setter方法中验证数据有效性

    • 示例:检查邮箱格式、密码强度

  3. 实现灵活性

    • 可以修改内部实现而不影响外部代码

    • 示例:将存储方式从数组改为链表

  4. 访问控制

    • 提供只读、只写或读写权限

    • 示例:身份证号只读,余额只通过方法修改

私有属性使用模式:Getter和Setter
cpp 复制代码
class User {
private:
    string username;
    string email;
    int age;
    double balance;

public:
    // 构造函数
    User(string name, string mail, int userAge) 
        : username(name), email(mail), age(userAge), balance(0.0) {
        // 初始化验证
        setAge(userAge);
        setEmail(mail);
    }

    // Getter方法 (访问属性)
    string getUsername() const { return username; }
    string getEmail() const { return email; }
    int getAge() const { return age; }
    double getBalance() const { return balance; }

    // Setter方法 (修改属性 - 带验证)
    void setEmail(string newEmail) {
        // 简单验证邮箱格式
        if (newEmail.find('@') == string::npos) {
            cout << "错误:无效的邮箱地址" << endl;
            return;
        }
        email = newEmail;
    }

    void setAge(int newAge) {
        if (newAge < 0) {
            cout << "错误:年龄不能为负数" << endl;
            return;
        }
        if (newAge > 150) {
            cout << "警告:年龄值异常大" << endl;
        }
        age = newAge;
    }

    // 业务方法(修改余额)
    void deposit(double amount) {
        if (amount <= 0) {
            cout << "错误:存款金额必须为正数" << endl;
            return;
        }
        balance += amount;
    }

    bool withdraw(double amount) {
        if (amount <= 0) {
            cout << "错误:取款金额必须为正数" << endl;
            return false;
        }
        if (amount > balance) {
            cout << "错误:余额不足" << endl;
            return false;
        }
        balance -= amount;
        return true;
    }
};
何时使用公有属性?

在极少数情况下,可以使用公有属性:

  1. 简单数据容器(如坐标点)

    cpp 复制代码
    struct Point {
        double x;
        double y;
    };
  2. 常量属性

    cpp 复制代码
    class Circle {
    public:
        const double PI = 3.14159; // 公有常量
    private:
        double radius;
    };
  3. 性能关键场景(极少见)

封装的最佳实践
  1. 黄金法则:所有属性默认设为私有

  2. 最小暴露原则:只暴露必要的方法

  3. 一致的命名规范

    • Getter:getProperty()property()

    • Setter:setProperty(value)

  4. 适当的const修饰

    cpp 复制代码
    // 不修改对象的方法应声明为const
    double getBalance() const { return balance; }
  5. 避免过度封装:不要为封装而封装

完整示例:银行账户类
cpp 复制代码
#include <iostream>
#include <string>
using namespace std;

class BankAccount {
private:
    string accountNumber;
    string ownerName;
    double balance;
    int pin; // 安全敏感数据

public:
    // 构造函数
    BankAccount(string number, string name, int securityPin)
        : accountNumber(number), ownerName(name), balance(0.0), pin(securityPin) {}
    
    // Getter方法
    string getAccountNumber() const { return accountNumber; }
    string getOwnerName() const { return ownerName; }
    double getBalance() const { return balance; }
    
    // 安全验证方法
    bool verifyPin(int inputPin) const {
        return inputPin == pin;
    }
    
    // 业务方法
    void deposit(double amount) {
        if (amount <= 0) {
            cout << "存款金额必须为正数!" << endl;
            return;
        }
        balance += amount;
        cout << "存款成功。当前余额: " << balance << endl;
    }
    
    bool withdraw(double amount, int inputPin) {
        if (!verifyPin(inputPin)) {
            cout << "PIN码错误!" << endl;
            return false;
        }
        if (amount <= 0) {
            cout << "取款金额必须为正数!" << endl;
            return false;
        }
        if (amount > balance) {
            cout << "余额不足!" << endl;
            return false;
        }
        balance -= amount;
        cout << "取款成功。当前余额: " << balance << endl;
        return true;
    }
    
    void changePin(int oldPin, int newPin) {
        if (!verifyPin(oldPin)) {
            cout << "旧PIN码错误!" << endl;
            return;
        }
        if (newPin < 1000 || newPin > 9999) {
            cout << "PIN码必须是4位数字!" << endl;
            return;
        }
        pin = newPin;
        cout << "PIN码修改成功!" << endl;
    }
};

int main() {
    // 创建账户
    BankAccount account("123456789", "张三", 1234);
    
    // 存款
    account.deposit(1000);
    
    // 尝试取款(错误PIN)
    account.withdraw(200, 1111); // 失败
    
    // 正确取款
    account.withdraw(200, 1234); // 成功
    
    // 修改PIN
    account.changePin(1234, 5678);
    
    // 尝试查看余额(无法直接访问)
    // cout << account.balance; // 错误:balance是私有的
    cout << "当前余额: " << account.getBalance() << endl;
    
    return 0;
}
封装总结
  1. 封装是保护:保护对象内部状态不被破坏

  2. 封装是控制:控制对数据的访问和修改方式

  3. 封装是契约:提供稳定的公共接口供外部使用

  4. 封装是自由:内部实现可以独立变化而不影响外部

记住面向对象设计的黄金法则:将变化封装在类内部。通过将成员属性设为私有,并提供受控的公共接口,你可以创建出健壮、灵活且易于维护的代码。

4.2. 继承

4.2.1 继承的基本语法

什么是继承?

继承是面向对象编程的核心概念之一,它允许我们创建一个新类(派生类)来继承 另一个类(基类)的属性方法。这种机制实现了代码的重用和层次化设计。

基本语法:
cpp 复制代码
class 派生类名 : 访问修饰符 基类名 {
    // 派生类新增成员
};
关键术语:
  • 基类(父类):被继承的类

  • 派生类(子类):继承基类的类

  • 访问修饰符:public、protected或private(决定继承方式)

简单示例:
cpp 复制代码
#include <iostream>
using namespace std;

// 基类:交通工具
class Vehicle {
public:
    void start() {
        cout << "交通工具启动..." << endl;
    }
    
    void stop() {
        cout << "交通工具停止..." << endl;
    }
};

// 派生类:汽车(继承自Vehicle)
class Car : public Vehicle {
public:
    void honk() {
        cout << "汽车鸣笛: 嘀嘀嘀!" << endl;
    }
};

int main() {
    Car myCar;
    myCar.start();  // 继承自Vehicle
    myCar.honk();   // Car自有方法
    myCar.stop();   // 继承自Vehicle
    
    return 0;
}
输出:
bash 复制代码
交通工具启动...
汽车鸣笛: 嘀嘀嘀!
交通工具停止...
继承的优势:
  1. 代码复用:避免重复编写相同代码

  2. 扩展功能:在基类基础上添加新功能

  3. 层次化设计:建立类之间的层次关系

  4. 多态基础:为多态性提供支持

4.2.2 继承方式

C++支持三种继承方式,它们决定了基类成员在派生类中的访问权限:

1. public继承(最常用)
  • 基类的public成员 → 派生类的public成员

  • 基类的protected成员 → 派生类的protected成员

  • 基类的private成员 → 不可访问

cpp 复制代码
class Base {
public:
    int publicVar;
protected:
    int protectedVar;
private:
    int privateVar;
};

class PublicDerived : public Base {
    // publicVar是public
    // protectedVar是protected
    // privateVar不可访问
};
2. protected继承
  • 基类的public成员 → 派生类的protected成员

  • 基类的protected成员 → 派生类的protected成员

  • 基类的private成员 → 不可访问

cpp 复制代码
class ProtectedDerived : protected Base {
    // publicVar是protected
    // protectedVar是protected
    // privateVar不可访问
};
3. private继承(极少使用)
  • 基类的public成员 → 派生类的private成员

  • 基类的protected成员 → 派生类的private成员

  • 基类的private成员 → 不可访问

cpp 复制代码
class PrivateDerived : private Base {
    // publicVar是private
    // protectedVar是private
    // privateVar不可访问
};
访问权限总结表:
基类成员 public继承 protected继承 private继承
public public protected private
protected protected protected private
private 不可访问 不可访问 不可访问
实际应用示例:
cpp 复制代码
class Animal {
public:
    void eat() { cout << "吃东西..." << endl; }
protected:
    void sleep() { cout << "睡觉..." << endl; }
private:
    void digest() { cout << "消化..." << endl; }
};

// public继承
class Dog : public Animal {
public:
    void bark() {
        eat();    // 可以访问(public)
        sleep();   // 可以访问(protected)
        // digest(); // 错误!private成员不可访问
    }
};

int main() {
    Dog myDog;
    myDog.eat();   // 可以访问
    myDog.bark();  // 可以访问
    // myDog.sleep(); // 错误!protected成员外部不可访问
    
    return 0;
}

4.2.3 继承中的对象模型

派生类对象的内存结构

当派生类继承基类时,派生类对象包含完整的基类子对象,再加上自己的成员。

cpp 复制代码
class Base {
public:
    int baseVar;
};

class Derived : public Base {
public:
    int derivedVar;
};

内存布局:

bash 复制代码
[Derived对象]
    [Base子对象]
        baseVar
    derivedVar

验证对象大小:

cpp 复制代码
#include <iostream>
using namespace std;

class A {
    int a; // 4字节
};

class B : public A {
    int b; // 4字节
};

class C : public B {
    int c; // 4字节
};

int main() {
    cout << "sizeof(A): " << sizeof(A) << endl; // 4
    cout << "sizeof(B): " << sizeof(B) << endl; // 8 (A的4字节 + B的4字节)
    cout << "sizeof(C): " << sizeof(C) << endl; // 12 (A+B的8字节 + C的4字节)
    
    return 0;
}
内存地址验证:
cpp 复制代码
C obj;
cout << "C对象地址: " << &obj << endl;
cout << "B子对象地址: " << (B*)&obj << endl; // 与C对象地址相同
cout << "A子对象地址: " << (A*)&obj << endl; // 与C对象地址相同
cout << "C::c地址: " << &obj.c << endl;     // 比对象地址高8字节
重要结论:
  1. 派生类对象包含完整的基类子对象

  2. 基类子对象位于派生类对象的起始位置

  3. 基类的私有成员也存在于派生类对象中,只是不可直接访问

4.2.4 继承中构造和析构顺序

构造函数调用顺序
  1. 基类构造函数(按照继承顺序)

  2. 成员对象构造函数(按照声明顺序)

  3. 派生类自身构造函数

析构函数调用顺序(完全相反)
  1. 派生类自身析构函数

  2. 成员对象析构函数

  3. 基类析构函数

完整示例:
cpp 复制代码
#include <iostream>
using namespace std;

class Base {
public:
    Base() { cout << "Base构造函数" << endl; }
    ~Base() { cout << "Base析构函数" << endl; }
};

class Member {
public:
    Member() { cout << "Member构造函数" << endl; }
    ~Member() { cout << "Member析构函数" << endl; }
};

class Derived : public Base {
private:
    Member mem; // 成员对象
public:
    Derived() { cout << "Derived构造函数" << endl; }
    ~Derived() { cout << "Derived析构函数" << endl; }
};

int main() {
    cout << "创建Derived对象..." << endl;
    Derived d;
    cout << "Derived对象即将销毁..." << endl;
    return 0;
}
输出:
bash 复制代码
创建Derived对象...
Base构造函数
Member构造函数
Derived构造函数
Derived对象即将销毁...
Derived析构函数
Member析构函数
Base析构函数
继承链中的构造顺序:
cpp 复制代码
class Grandparent {
public:
    Grandparent() { cout << "Grandparent构造函数" << endl; }
};

class Parent : public Grandparent {
public:
    Parent() { cout << "Parent构造函数" << endl; }
};

class Child : public Parent {
public:
    Child() { cout << "Child构造函数" << endl; }
};

int main() {
    Child c;
    return 0;
}

输出:

bash 复制代码
Grandparent构造函数
Parent构造函数
Child构造函数
重要注意事项:
  1. 如果基类没有默认构造函数,派生类必须显式调用基类构造函数

    cpp 复制代码
    class Base {
    public:
        Base(int x) { /* ... */ }
    };
    
    class Derived : public Base {
    public:
        Derived() : Base(10) { // 必须显式调用基类构造函数
            // ...
        }
    };
  2. 析构函数应该声明为virtual(在多态场景中尤其重要)

    cpp 复制代码
    class Base {
    public:
        virtual ~Base() { // 虚析构函数
            cout << "Base析构" << endl;
        }
    };

4.2.4 继承中构造和析构顺序

问题场景

当派生类定义了与基类同名的成员(变量或函数)时,会发生名称隐藏(name hiding)。

处理规则:
  1. 派生类成员会隐藏基类的同名成员

  2. 通过作用域解析运算符::访问基类成员

同名成员变量处理:
cpp 复制代码
#include <iostream>
using namespace std;

class Base {
public:
    int value = 100;
};

class Derived : public Base {
public:
    int value = 200; // 与基类同名
    
    void showValues() {
        cout << "派生类value: " << value << endl;          // 200
        cout << "基类value: " << Base::value << endl;      // 100
    }
};

int main() {
    Derived d;
    d.showValues();
    cout << "外部访问: " << d.value << endl;               // 200
    // cout << d.Base::value << endl;                      // 可以访问
    return 0;
}
同名成员函数处理:
cpp 复制代码
class Base {
public:
    void display() {
        cout << "Base display" << endl;
    }
};

class Derived : public Base {
public:
    // 隐藏基类的display函数
    void display() {
        cout << "Derived display" << endl;
    }
    
    void show() {
        display();          // 调用Derived::display
        Base::display();    // 调用Base::display
    }
};

int main() {
    Derived d;
    d.display();    // 调用Derived::display
    d.Base::display(); // 调用Base::display
    return 0;
}
函数重载的特殊情况:

如果基类有重载函数,派生类定义同名函数会隐藏基类的所有重载版本:

cpp 复制代码
class Base {
public:
    void func() { cout << "Base::func()" << endl; }
    void func(int) { cout << "Base::func(int)" << endl; }
};

class Derived : public Base {
public:
    // 隐藏基类的所有func函数
    void func() { cout << "Derived::func()" << endl; }
};

int main() {
    Derived d;
    d.func();       // 正确:Derived::func()
    // d.func(10); // 错误:基类的func(int)被隐藏
    d.Base::func(10); // 正确:显式调用基类函数
    return 0;
}
解决方案:使用using声明
cpp 复制代码
class Derived : public Base {
public:
    using Base::func; // 引入基类的所有func函数
    
    void func() { 
        cout << "Derived::func()" << endl; 
    }
};

int main() {
    Derived d;
    d.func();      // Derived::func()
    d.func(10);    // Base::func(int)
    return 0;
}
最佳实践:
  1. 避免不必要的名称隐藏

  2. 使用using声明引入基类函数

  3. 使用作用域解析运算符解决冲突

  4. 为重要函数使用不同名称

综合示例:图形类层次

复制代码
#include <iostream>
#include <string>
using namespace std;

// 基类:图形
class Shape {
private:
    string color;
public:
    Shape(string c) : color(c) {}
    
    string getColor() const { return color; }
    
    virtual void draw() const {
        cout << "绘制一个" << color << "色的图形" << endl;
    }
    
    virtual double area() const = 0; // 纯虚函数
};

// 派生类:圆形
class Circle : public Shape {
private:
    double radius;
public:
    Circle(string c, double r) : Shape(c), radius(r) {}
    
    void draw() const override {
        cout << "绘制一个" << getColor() << "色的圆形" << endl;
    }
    
    double area() const override {
        return 3.14159 * radius * radius;
    }
};

// 派生类:矩形
class Rectangle : public Shape {
private:
    double width;
    double height;
public:
    Rectangle(string c, double w, double h) 
        : Shape(c), width(w), height(h) {}
    
    void draw() const override {
        cout << "绘制一个" << getColor() << "色的矩形" << endl;
    }
    
    double area() const override {
        return width * height;
    }
};

int main() {
    Circle redCircle("红", 5.0);
    Rectangle blueRect("蓝", 4.0, 6.0);
    
    redCircle.draw();
    cout << "圆形面积: " << redCircle.area() << endl;
    
    blueRect.draw();
    cout << "矩形面积: " << blueRect.area() << endl;
    
    // 多态使用
    Shape* shapes[] = {&redCircle, &blueRect};
    for (Shape* s : shapes) {
        s->draw();
        cout << "面积: " << s->area() << endl;
    }
    
    return 0;
}

继承总结

  1. 基本语法class Derived : access-specifier Base

  2. 继承方式:public(最常用)、protected、private

  3. 对象模型:派生类包含完整的基类子对象

  4. 构造顺序:基类 → 成员对象 → 派生类

  5. 析构顺序:派生类 → 成员对象 → 基类

  6. 同名成员 :派生类成员隐藏基类成员,使用Base::member访问

掌握这些继承概念是理解C++面向对象编程的关键。实际开发中,建议:

  • 优先使用public继承

  • 将基类析构函数声明为virtual

  • 使用override关键字明确重写虚函数

  • 避免过度复杂的继承层次

  • 考虑使用组合代替继承("组合优于继承"原则)

4.2.5 继承同名成员处理方式

4.2.6 非自动继承的函数(还有自动继承得到函数?)

4.2.6 继承同名静态成员处理方式

4.2.7 多继承语法

4.2.8 菱形继承

4.3. 多态

4.3.1 多态的基本概念

4.3.2 多态案例一-计算器类

4.3.3 纯虚函数和抽象类

4.3.4 多态案例二-制作饮品

4.3.5 虚析构和纯虚析构

4.3.6 多态案例三-电脑组装

4.4 对象的初始化和清理

4.4.1 构造函数和析构函数

4.4.2 构造函数的分类及调用

4.4.3 拷贝构造函数调用时机

4.4.4 构造函数调用规则

4.4.5 深拷贝与浅拷贝

4.4.6 初始化列表

4.4.7 类对象作为类成员

4.4.8 静态成员

4.5 C++对象模型和this指针

4.5.1 成员变量和成员函数分开存储

4.5.2 this指针概念

4.5.3 空指针访问成员函数

4.5.4 const修饰成员函数

4.6 友元

4.6.1 全局函数做友元

4.6.2 类做友元

4.6.3 成员函数做友元

4.7 运算符重载

4.7.1 加号运算符重载

4.7.2 左移运算符重载

4.7.3 递增运算符重载

4.7.4 赋值运算符重载

4.7.5 关系运算符重载

4.7.6 函数调用运算符重载

5.面试题

重写,重载,重定义分别是什么意思?多态是什么意思

相关推荐
LUCIAZZZ14 小时前
Java设计模式基础问答
java·开发语言·jvm·spring boot·spring·设计模式
大白爱琴17 小时前
八股文——JVM
java·jvm·spring
2301_794333911 天前
Maven 概述、安装、配置、仓库、私服详解
java·开发语言·jvm·开源·maven
黄雪超1 天前
JVM——对象模型:JVM对象的内部机制和存在方式是怎样的?
java·开发语言·jvm
用户7468160182611 天前
java项目假死问题排查
jvm
子豪-中国机器人1 天前
C++ 信息学奥赛总复习题
java·jvm·算法
yt948321 天前
JVM如何优化
jvm
wodownload22 天前
CS003-2-2-perfermance
java·开发语言·jvm
重庆小透明2 天前
【从零学习JVM|第三篇】类的生命周期(高频面试题)
java·jvm·后端·学习