news 2026/7/13 2:59:42

C++学习(9):继承与多态-auto、引用、const变量和const函数

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C++学习(9):继承与多态-auto、引用、const变量和const函数
//2. 继承与多态(虚函数) //基类 `LogicGate`,派生 `AndGate` 和 `OrGate`。 #include <iostream> #include <vector> #include <memory> class LogicGate { public: virtual ~LogicGate() = default; virtual bool compute(bool a, bool b) const = 0; // 纯虚函数 virtual const char* name() const = 0; }; class AndGate : public LogicGate { public: bool compute(bool a, bool b) const override { return a && b; } const char* name() const override { return "AND"; } }; class OrGate : public LogicGate { public: bool compute(bool a, bool b) const override { return a || b; } const char* name() const override { return "OR"; } }; int main() { std::vector<std::unique_ptr<LogicGate>> gates; gates.push_back(std::make_unique<AndGate>()); gates.push_back(std::make_unique<OrGate>()); bool a = true, b = false; for (const auto& gate : gates) { std::cout << gate->name() << ": " << a << " " << b << " -> " << gate->compute(a, b) << std::endl; } return 0; }

零、序言

Q:来教我读这个代码。我能明白这是关于函数继承,也就是,在原本那个类里面写一个虚函数的空壳,然后继承那个类以后在虚函数里填入自己的函数,相当于在更基础的类里包装了接口。虽然我懂这个东西,但是我不理解里面的一些很小的语法知识点,例如auto、make_unique这种c语言里没有或不常见的东西

A:理解的概念层面完全正确:LogicGate定义了一套“接口契约”,AndGateOrGate履行了这套契约。这确实是面向对象多态的核心思想。

1.auto(自动类型推导)

C语言视角:在C语言中,你声明变量时必须明确写出类型,比如int i = 0;

C++ 的auto:它的作用是让编译器根据初始化表达式的类型,自动推断出变量的类型。它不是“动态类型”(运行时改变),而是“编译时自动填充”。

在你的代码里:

cpp

for (const auto& gate : gates) { ... }

编译器看到gates是一个std::vector<std::unique_ptr<LogicGate>>,它会自动推断出gate的类型应该是:
const std::unique_ptr<LogicGate>&(也就是一个指向LogicGate智能指针的常量引用)。
你可以把它理解为“打字省力工具”,但更重要的作用是让代码在模板编程中更通用。你不需要记住那个长长的类型名,编译器会帮你填好。


2.std::make_unique<AndGate>()(创建智能指针)

C语言视角:在C语言中,你会在堆上分配结构体,比如struct AndGate* p =(struct AndGate*)malloc(sizeof(struct AndGate));,然后手动free

C++ 的make_unique:它是一种安全、简洁地在堆上创建对象并返回“独占所有权”智能指针的函数

  • new AndGate()相当于malloc+ 调用构造函数。

  • std::make_unique<AndGate>()则直接在堆上构造一个AndGate对象,并把它包装进一个std::unique_ptr<AndGate>智能指针里返回。

  • 为什么要这么写?因为std::unique_ptr是一个“独占指针”,它离开作用域时,会自动delete它指向的对象。你永远不需要手动写delete(对应C语言的free),从而杜绝了内存泄漏。

  • 这里的std::make_unique是 C++14 引入的,如果是C++11,通常会用std::unique_ptr<AndGate>(new AndGate()),但前者更安全(避免某些异常导致的泄漏)。


3.override(显式重写标记)

C语言视角:C语言没有类的继承,自然也没有这个概念。

C++ 的override:这是一个编译时检查的关键字。它的作用是告诉编译器:“我明确声明,这个函数是要重写(覆盖)基类里的那个虚函数。”。
如果你的函数名拼错了,或者参数类型写错了(导致没有匹配到基类的虚函数),编译器就会直接报错,而不是悄悄地把它当作一个全新的独立函数。
在你的代码里:

cpp

bool compute(bool a, bool b) const override { ... }

这确保了compute确实匹配了基类LogicGate中的compute。如果不写override,代码也能运行,但如果你不小心把参数bool b写成了bool c,编译器不会提示你,你会得到一个奇怪的“隐藏”错误。加了override,编译器会立刻拦住你。


4.virtual ~LogicGate() = default;= 0

C语言视角:C语言没有析构函数的概念。

= default:告诉编译器“使用默认生成的析构函数版本”。因为基类有虚函数,析构函数必须声明为virtual,这样才能保证当你删除一个指向子类的基类指针时,子类的析构函数也能被正确调用。这里= default就是“我不需要写特殊清理代码,编译器按最标准的方式生成就好”。

= 0:这是纯虚函数的标志。就是你在概念上理解的“空壳接口”。它告诉编译器“这个函数在这个类里没有实现,任何继承我的具体类必须自己实现它”。这也使得LogicGate成了一个“抽象类”,你不能直接创建LogicGate的对象(如new LogicGate()),只能创建它的子类对象。


5.for (const auto& gate : gates)(范围 for 循环)

C语言视角:C语言里你要遍历数组,得写for (int i = 0; i < n; i++) { ... }

C++ 的范围 for 循环:这是 C++11 引入的语法糖,专门用来遍历容器(如vector、数组)。

  • gates:要遍历的容器。

  • gate:每一次循环拿到的元素。

  • constauto&:表示gate元素的常量引用(不拷贝,只读)。如果写成auto gate,它会复制一份智能指针,虽然无伤大雅,但浪费性能。使用const&(常量引用)是 C++ 的惯用法,表示“只读取,不改动,且不复制”。
    它在底层翻译成的逻辑就是:

cpp

for (auto it = gates.begin(); it != gates.end(); ++it) { const auto& gate = *it; // ... }

但范围 for 循环把样板代码省掉了,可读性更高。


6. 成员函数后面跟const

这是一个 C++ 特有的小细节:

cpp

virtual bool compute(bool a, bool b) const = 0;

末尾的const表示这个函数承诺不会修改类的成员变量当你有一个const LogicGate&引用时,你只能调用那些带const后缀的成员函数。这在 EDA 代码里经常见到,用来确保“读数据”的操作不会意外改动电路对象的状态,起到了“自文档化”和“编译时约束”的作用。


总结:把这些点串起来

语法点你可以理解为(C语言的近似)
auto编译器帮你写int/string/指针类型
make_uniquemalloc+ 自动在末尾插入free的保姆
override给编译器的一个“请帮我检查我有没有拼错父类函数名”的指令
= 0这个函数在父类里没代码,子类必须自己写
for (auto& x : list)不用自己数下标的for循环
函数后的const保证这个函数不会修改对象内部数据

你之前的“虚函数空壳”概念完全正确。上面这些只是围绕这个机制存在的现代 C++ 工具箱。写 EDA 算法时,你几乎天天都会和autounique_ptroverride打交道,但它们的用法基本就是你现在看到的这个样子。你第一次接触能看懂到这个程度,已经非常好了。

一、auto-自动类型推导;引用

Q:我们首先来看看auto吧,你的意思是说,auto是一个自动类型识别?给我举一些简单的例子看看。

A:理解得完全正确:auto就是“自动类型推导”(Automatic Type Deduction)。它让编译器根据你赋给它的值,自动推断出这个变量的类型。

但是请记住一个最重要的区别:它是编译时确定的,不是运行时。也就是说,一旦编译器推断出类型,这个变量就和手写类型的变量一模一样,没有任何运行时性能开销。它只是帮我们省去了打字的功夫。

1. 基础例子:代替基本类型

cpp

#include <iostream> #include <string> int main() { // 传统写法:手动写类型 int a = 5; double b = 3.14; std::string c = "hello"; // auto 写法:让编译器猜 auto x = 5; // x 的类型是 int auto y = 3.14; // y 的类型是 double auto z = "hello"; //注意:这里 z 的类型是 const char*(字符串字面量),不是 std::string// 但如果是从 std::string 变量初始化,则是 std::string std::string s = "world"; auto t = s; // t 的类型是 std::string std::cout << x << " " << y << " " << t << std::endl; return 0; }

结论:它就像填空,你把值放右边,编译器把类型填左边。


2. 指针和引用的区别(这点很重要)

cpp

int main() { int num = 10; int* p = &num; // 传统指针 auto p1 = &num; // p1 的类型是 int* auto* p2 = &num; // p2 的类型也是 int*(明确写了 *,更清晰) int& ref = num; // 传统引用 auto ref1 = num; // ref1 的类型是 int(不是引用!因为auto 默认会去掉引用,直接复制值) auto& ref2 = num; // ref2 的类型是 int&(加上 & 才能保留引用特性) ref2 = 20; // 修改 ref2 会直接改变 num // ref1 = 30; // 如果 ref1 是 int,修改它不会影响 num return 0; }

关键规则auto默认会剥离引用(&)和顶层const(常量属性)。如果你想要引用,必须显式写成auto&


3. 在遍历容器时(你代码里的用法)

cpp

#include <vector> #include <string> int main() { std::vector<std::string> names = {"Alice", "Bob", "Charlie"}; // 写法 1:复制一份(如果字符串很长,拷贝会浪费性能) for (auto name : names) { // name 是 std::string,是 names 里元素的副本 } // 写法 2:只读引用(不拷贝,高效,且不能修改原数据) for (const auto& name : names) { // name 是 const std::string&,指向容器里的原始字符串 std::cout << name; } // 写法 3:可修改的引用(可以修改容器里的数据) for (auto& name : names) { name = "Mr. " + name; // 直接修改原数据 } return 0; }

4.auto最有用的场景:告别极其冗长的类型名

如果你不用auto,在处理容器迭代器时,你需要写很长一串:

cpp

#include <vector> #include <map> int main() { std::map<std::string, std::vector<int>> complex_data; // 不用 auto 的痛苦写法: std::map<std::string, std::vector<int>>::iterator it1 = complex_data.begin(); // 用了 auto 的快乐写法: auto it2 = complex_data.begin(); // 类型完全一样,但代码短了一大截 return 0; }

Q:奇怪,所以引用是什么样的?当你写auto& p=a,它是把p的地址也指向a的地址了吗?

A:当你写auto& p = a;时,是“p 和 a 共享同一个地址”。

为了让你彻底明白,我们分三步来看:概念上、代码上、底层上


1. 概念上的区别:引用是“别名”,不是“指针”

  • 指针int* p = &a;——p是一个独立的变量,它的a的地址。要拿到a,你必须解引用(*p)。

  • 引用int& p = a;——p不是一个独立的变量,它是a别名使用p就是直接使用a,不需要解引用。

当你写auto& p = a;,编译器只是记住了:“以后提到p,就等同于提到a”。它并没有创建一个新的变量来存储a的地址。


2. 代码验证:取地址运算符&会告诉你真相

我们写一段代码来验证引用和指针的差异:

cpp

#include <iostream> int main() { int a = 10; int* ptr = &a; // 指针:存储 a 的地址 int& ref = a; // 引用:a 的别名 // 1. 打印它们的值 std::cout << "a 的值: " << a << std::endl; // 10 std::cout << "*ptr 的值: " << *ptr << std::endl; // 10(需要解引用) std::cout << "ref 的值: " << ref << std::endl; // 10(直接当 a 用) // 2. 关键证据:打印它们的地址! std::cout << "&a = " << &a << std::endl; // 假设地址是 0x7fff... std::cout << "&ref = " << &ref << std::endl; // 和 &a 完全一样!!! std::cout << "&ptr = " << &ptr << std::endl; // 指针自己的地址,和 &a 不同! // 3. 修改 ref,观察 a ref = 20; std::cout << "修改 ref 后,a = " << a << std::endl; // 变成 20 了 // 因为 ref 就是 a,所以修改 ref 就是修改 a return 0; }

运行你会发现:&a&ref打印出的地址一模一样。这就证明了引用不是“指向”目标,而是“就是”目标本身。


3. 底层实现(汇编/内存视角)

在底层(机器码层面),编译器确实是用地址来实现引用的。当你写ref = 20时,编译器生成的机器码很可能是“将数值 20 写入地址&a所在的内存位置”。

区别在于

  • 指针ptr:它占一块内存(存地址),它有自己独立的地址(&ptr)。

  • 引用ref:它不占用独立的内存空间(严格来说是它“没有”自己的地址)。它的出现,在底层就会被编译器解释为“直接操作a的地址”。

所以回到你的原话“把 p 的地址也指向 a 的地址”

  • 如果你把p想象成“存放地址的盒子”,那么这个说法在底层近似成立(编译器确实会用地址去处理)。

  • 但从 C++ 语法上说,这个说法不成立,因为p本身不是一个“存放地址的盒子”,它就是a的另一个名字。你没有办法像修改指针那样,让p去指向另一个变量(引用一旦绑定,终身不变)。


4. 回到你的代码auto& p = a

auto在这里只是推导出了a的类型(int),加上&后,p就变成了int&(整型引用)。
所以auto& p = a;等价于int& p = a;。它就是给a起了一个新名字叫p,两个名字共享同一块内存。当你在循环里写const auto& gate : gates时,gate就是gates里那个元素的别名,所以不会发生拷贝,并且只读。

Q:也就是说,并没有建立一个新的p指针单元用于存放a的地址?

A:是的。

底层实现:那编译器和 CPU 怎么做到的?

你可能会想:“既然没有存放地址的单元,那计算机运行时怎么知道ref对应哪个内存位置?”
答案是:编译器在编译时把“引用”翻译成了“直接操作原变量”

当你在代码里写ref = 20;时,编译器生成的机器码相当于直接写a = 20;。它在底层可能会使用地址寻址,但它不会ref单独分配一块内存来存储地址。

唯一的例外:当引用作为函数参数时,编译器底层确实是通过传递“地址”来实现的(避免拷贝大对象)。但在语法层面上,你依然不需要把它当成指针来用——你不需要解引用(*ref),也不需要用&去取地址才能赋值。它依然表现得像“我就是那个对象本身”

一句话总结

  • 指针:是一个独立变量,存放地址。

  • 引用:是已有变量的别名,没有独立的存储空间。你auto&看作“我只是给这个变量贴了个新标签”就完全对了

二、const char* name() const-常量指针和常量函数

Q:const char* name() const
我想知道这一小段是什么意思,以及再给我详细讲讲这两个const吧

A:

这行代码const char* name() const,是 C++ 里一个非常经典的声明,充满了“契约精神”。它混合了“返回值类型”和“函数行为约束”两个维度。

别怕,我们把它拆成两部分来看:返回值的const函数末尾的const


1. 返回值部分的const(函数名左边的):const char*

这是函数的返回类型,意思是:这个函数返回一个指向“常量字符”的指针。

  • 如果没有const:返回char*,调用者可以通过这个指针去修改它指向的内存内容。

  • 有了const:返回const char*,告诉调用者:“我给你这个字符串的地址,但你只能读它,绝对不能修改它。”

在 EDA 代码里,这个函数返回的是"AND""OR"。这些是字符串字面量(比如"AND"),它们通常被存放在程序的只读数据段里。如果你试图通过指针去修改它们,程序会崩溃。

所以这里写const char*是一个双重保护

  1. 物理上:字符串字面量本身是只读的,改不了。

  2. 语法上:即使你有坏心思,编译器也会因为const而直接报错,不让你编译通过。


2. 函数末尾的const(函数名右边的):... name() const

这是 C++ 特有(C语言完全没有)的语法,叫做const成员函数。它的作用是:承诺这个函数不会修改类的任何成员变量。

当你写下const在括号后面时,编译器会强制执行这个承诺:

  • 在这个函数体内部,你不能修改m_name(假设存在)、m_value或任何其他非静态成员变量。

  • 你只能读取它们。

为什么要加这个尾巴?
因为 C++ 允许你创建const对象(比如const LogicGate& gate)。只有带有末尾const的成员函数,才能被const对象调用。

在你的代码里:

cpp

for (const auto& gate : gates) { // gate 是常量引用 std::cout << gate->name(); // 调用了 name() const // 如果 name() 没有末尾的 const,这一行就会编译报错! }

因为我们是只读遍历(const auto&),不打算修改任何门电路的信息。所以name()必须加上末尾的const,才能配合这种“只读访问”的用法。


3. 两个const的区别(对比表格)

特征左边的const(返回值)右边的const(成员函数)
作用于谁影响函数外部的调用者影响函数内部的行为
约束什么约束“返回值”本身能否被修改约束“成员变量”能否在这个函数里被修改
谁会被限制拿到返回值的调用者写这个函数的开发者(你自己)
C语言有吗?有,const char*在 C 里也有没有,这是 C++ 专有的类特性
记忆口诀“我给你的字符串,你只能看,不能改”“我叫 name 的时候,绝不会动类的内部状态”

4. 用对比代码看清楚它们的分工

假设你的类里有一个std::string m_name;

cpp

class Example { private: std::string m_name; public: // 错误示范:返回值没有 const,末尾也没有 const char* badName() { return &m_name[0]; } // 调用者可以改 m_name,且不能用于 const 对象 // 正确示范:返回常量指针 + 末尾 const const char* goodName() const { // 1. 末尾 const 保证这里不能写 m_name = "xxx"; // 2. 返回 const char*,保证调用者不能改 m_name return m_name.c_str(); } };

在 EDA 工具里,你经常需要把电路的信息(比如门类型)打印出来,但绝不允许打印的时候意外改掉电路的状态。所以这种const char* name() const的模式是业内标准写法

总结一句话:左边的const保护数据不被外部改,右边的const保护对象内部不被函数改。两个合在一起,就是“我安全地告诉你我的名字,你安心读,我保证不改自己”。

Q:没太理解。你的意思是说,我们这里函数的返回值是一个const常量的char指针,指针的名字叫name?而后,如果我们写了一个成员函数的const,我们就不能在这个函数里面写name="一串字符"?是这个意思吗?主要是,你说保护变量不被修改,这点我理解,因为你举的例子确实需要变量不被修改,这个代码也没修改and门or门的名字。但是,函数体内,return "AND"又是什么呢,它并不是在对char进行赋值吗?如果是不就是修改了吗,如果不是那这个实例里关于对象的名字变量又是什么呢?还是说,这种写法避开了写出一个显示的名字变量,就为了名字变量不被更改?

A:

1. 纠正一个小误解:name是函数名,不是指针名

你问“指针的名字叫name”——不,name是函数名,不是变量名。这行声明应该按顺序读:

const char*(返回类型) +name(函数名)+( )(无参数)+const(尾部修饰符)

返回的那个指针是临时的,没有名字。在maingate->name()拿到这个指针后,直接送给std::cout打印,这个指针用完就消失了,没有名字。


2.return "AND"是在“赋值”吗?如果不是,那它的本质是什么?

不是赋值,是“返回地址”。

  • "AND"是一个字符串字面量(String Literal)。它不是一个普通的变量,它在编译时就被写进了你的可执行文件里(放在只读数据段,比如.rodata区域)

  • 当程序运行时,这段内存已经存在了,它有一个固定的内存地址(比如0x400123)。

  • return "AND";所做的,就是把这个固定的内存地址返回给调用者。它没有创建新字符串,没有复制字符,只是在传递一个地址。

所以,我们并没有修改任何东西,也没有往一个变量里“写”字符串。我们只是说:“name()这个函数的返回值,就是"AND"这个字符串的地址。”


3. 那个“对象的名字变量”在哪里?

在这个代码里,根本没有一个叫m_name的成员变量。

这是一个纯接口类LogicGate),它只规定了“你必须有name()这个函数”,但没有规定“你必须用一个变量来存名字”。

  • AndGate选择直接返回字面量"AND"

  • OrGate选择返回"OR"

它们没有在对象内部存名字,而是把名字“固化”在了代码里。这节省了内存(每个对象少了一个std::string的开销),并且让代码极简。所以你说“避开了写出一个显式的名字变量”——完全正确!这就是一种设计策略。


4. 为什么返回值必须加const?(这就是你直觉里最核心的部分)

既然"AND"在只读数据段,如果你返回char*(不带const),调用者可能会尝试去写它:

cpp

char* p = gate->name(); // 假设没有 const p[0] = 'X'; // 尝试把 "AND" 改成 "XND"

这会导致程序崩溃(因为往只读内存里写数据)。

所以,const char*是在保护这块底层物理内存不被意外篡改。它告诉调用者:“这块内存是只读的,你如果想写,编译器会直接拒绝你。”


5. 末尾的const(尾巴上的)到底在保护什么?

尾巴上的const... name() const)保护的是类里的成员变量

在这个例子里,LogicGate类没有成员变量,所以这个尾巴看起来“没用”。但是,如果未来我们给AndGate加了一个int m_fanout成员变量,情况就变了:

cpp

class AndGate : public LogicGate { private: int m_fanout; // 假设有了这个变量 public: const char* name() const { // m_fanout = 10; // 错误!因为有尾巴 const,这里不能修改成员变量return "AND"; } void setFanout(int f) { m_fanout = f; // 正确!没有尾巴 const,这里可以修改 } };

尾巴上的const保证了:当你通过一个常量引用(const auto& gate)去调用name()时,这个函数绝对不会偷偷改动m_fanout或任何其他状态。这使得只读访问变得绝对可靠。


6. 用一句话总结你的困惑

return "AND"并不是在赋值,而是在提交一块已经存在的只读内存的地址。因为这块内存是只读的,所以返回类型必须是const char*来声明这一点。尾部的const则是用来确保这个函数在执行过程中不会改动类的内部数据。”

在这个场景里,对象里没有名字变量,名字被“硬编码”在代码段里。写const不是为了保护变量(因为压根没变量),而是为了保护数据段里那块只读内存,以及保证只读调用的安全性。你的直觉“避开了写出一个显式的名字变量”非常准确——这正是工程师为了性能和极简设计所做的选择。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/13 2:59:26

快慢指针法深度解析:解决链表环、中点、倒数第K节点3类问题的统一思路

快慢指针法深度解析&#xff1a;解决链表环、中点、倒数第K节点3类问题的统一思路链表操作是算法面试中的高频考点&#xff0c;而快慢指针法则是解决链表问题的利器。本文将深入剖析快慢指针的核心思想&#xff0c;展示如何用这一技巧优雅解决链表环检测、寻找中点、定位倒数第…

作者头像 李华
网站建设 2026/7/13 2:58:53

遗传算法实战调参指南:从跑不起来到工业级收敛

1. 项目概述&#xff1a;这不是又一篇“遗传算法入门”——而是你真正能跑通、调明白、用得上的第二课“遗传算法入门”这个词&#xff0c;我见得太多。打开网页&#xff0c;十篇里八篇是照搬生物类比&#xff1a;染色体二进制串&#xff0c;基因0或1&#xff0c;选择像自然淘汰…

作者头像 李华
网站建设 2026/7/13 2:57:52

APK Installer终极指南:3步在Windows上轻松安装Android应用

APK Installer终极指南&#xff1a;3步在Windows上轻松安装Android应用 【免费下载链接】APK-Installer An Android Application Installer for Windows 项目地址: https://gitcode.com/GitHub_Trending/ap/APK-Installer 想在Windows电脑上运行Android应用却不想安装臃…

作者头像 李华
网站建设 2026/7/13 2:50:58

ESP8266 STA/AP模式 3 种物联网应用场景实测:MQTT上报、TCP服务器、UDP广播

ESP8266双模实战&#xff1a;从MQTT上报到UDP组网的物联网开发指南在智能家居和工业物联网应用中&#xff0c;ESP8266凭借其出色的性价比和稳定的WiFi连接能力&#xff0c;已成为开发者首选的无线通信模块。不同于市面上大多数教程仅停留在基础AT指令讲解&#xff0c;本文将深入…

作者头像 李华
网站建设 2026/7/13 2:50:52

Overleaf 2024 协作功能深度解析:3人团队论文实战与 Git 集成指南

Overleaf 2024 协作功能深度解析&#xff1a;3人团队论文实战与 Git 集成指南1. 为什么团队需要专业的LaTeX协作工具在科研合作日益国际化的今天&#xff0c;跨机构、跨时区的学术写作已成为常态。传统本地LaTeX环境面临三大痛点&#xff1a;版本混乱&#xff08;如反复传递的p…

作者头像 李华