现在是6月16号,差不多24个小时后进行面向对象考试,这是我的考前速通笔记。
差不多零基础吧一学期一点没学()
(考完补充:这门课厦门大学还是考得很easy的,速通24小时拿下95,耶。)
课本1~3章都是弱智语法直接skip了。
第4章末尾开始讲一点C++。
实际上可以直接从第5章开始学。
一. C/C++语法回顾
数据类型
基本数据类型包括:int、char、float、double等
构造数据类型包括:数组、指针、结构体、类等

1. 形参实参
// 函数定义 - 制定快递流程void deliverPackage(string address, string recipient) { // address和recipient是形参 cout << "送货到:" << address << endl; cout << "收件人:" << recipient << endl;}
// 函数调用 - 实际执行快递任务int main() { deliverPackage("北京市朝阳区", "张三"); // "北京市朝阳区"和"张三"是实参 deliverPackage("上海市浦东区", "李四"); // "上海市浦东区"和"李四"是实参 return 0;}2. 引用
引用是C++在C语言基础上新增的特性。C语言只有指针来实现间接访问。
什么是引用?引用就像给一个人起别名。你的真名是”张三”,但朋友们可以叫你”小张”,这两个名字指的是同一个人。
int original = 100; // 原始变量int& nickname = original; // nickname 是 original 的引用(别名)
// 现在 original 和 nickname 指向同一块内存cout << original << endl; // 输出:100cout << nickname << endl; // 输出:100
nickname = 200; // 通过别名修改值cout << original << endl; // 输出:200 (原始变量也被改变了!)数据类型& 引用名 = 已存在的变量;二. C++函数的进一步讨论
1. 带参数宏
2. 内联函数
在程序中,一个常规的函数调用如下:
- 保存现场:程序需要把当前的指令地址、用到的变量等信息压入一个叫做“栈 (Stack)”的内存区域。
- 跳转:CPU 跳转到函数代码所在的内存地址。
- 执行函数:执行函数体内的代码。
- 返回:将返回值(如果有的话)放到指定位置。
- 恢复现场:从栈中弹出之前保存的信息,回到调用前的位置继续执行。
对于大部分函数来说,这个跳转的开销微不足道。但如果一个函数非常简单(比如就一行 return a + b;),并且被频繁调用(比如在循环里调用一万次),那么这数万次的“跳转”和“返回”所累积起来的开销就会变得很可观,影响程序性能。
所以可以用内联函数。
当我们请求编译器将一个函数作为内联函数处理时,编译器会在编译阶段,将每一次调用该函数的地方,都用函数体的实际代码来替换。
// 请求将 add 函数内联inline int add(int a, int b) { return a + b;}
int main() { int sum = add(5, 3); // 调用内联函数 return 0;}
// 编译器看到了 inline 请求,并且觉得 add 函数足够简单// 于是它把函数调用直接替换掉了
int main() { // int sum = add(5, 3); 被替换成了下面的代码 int sum = 5 + 3; return 0;}很容易想到,那递归函数也能直接替换吗?很明显不能。它都不知道自己要做几次,怎么替换呢?真替换了,编译阶段就会发生无限循环。所以编译器会直接放弃内联。
给函数定义加上关键词 inline 只是建议编译程序把该函数作为内联函数来处理,至于编译程序是否把它作为内联函数来实现,要视函数体的具体实现而定。比如递归函数就是加了也没用。
除此之外,因为编译器是分块编译再链接的,一个 cpp文件生成一个obj文件,最后用链接器链接起来,才生成exe。如果用inline,因为要内联时在编译时直接替换,所以是没有生成独立的函数地址的。因此一个内联函数不能在其他文件中被调用,除非通过#include 放进来。
3. 函数参数默认值
C++允许给函数参数加默认值,这样调用的时候,如果这些有默认值的参数没指定值,就直接用默认参数。
除了带默认值的参数必须放在形参表的最右侧外,还有一些注意点。
首先,函数的默认参数是给 调用者 (caller) 看的,而不是给函数定义看的。是编译器在编译调用函数的代码时需要。当编译器看到一个函数调用,比如 my_print();,但这个函数实际上是 void my_print(int level);,编译器需要一个默认值来填补那个被省略的参数。函数的定义(函数体 {...})是函数的具体实现,是不关心,也看不见这个默认值的。
举例:
// --- logger.h ---// 函数声明:在这里为 level 参数指定默认值 0void log(const char* message, int level = 0);
// --- logger.cpp --
// 函数定义:这里不需要也不能重复指定默认值// 它只负责实现功能void log(const char* message, int level) { std::cout << "[Level " << level << "]: " << message << std::endl;}(除非这个函数定义的时候也做声明,那直接写默认值是可以的!)
除此之外,还有个比较危险,实现的时候不推荐东西:在不同的源文件中,对同一个函数的声明可以为它的同一个参数指定不同的默认值。
这个规则揭示了一个 C++ 的“陷阱”。因为编译器是独立编译每个 .cpp 文件(翻译单元)的,所以理论上你可以这样做:
// --- uploader.cpp ---#include <iostream>void upload(const char* filename, int timeout = 10); // 声明1:超时默认10秒
void perform_upload() { upload("data.zip"); // 编译器看到默认值10,实际调用 upload("data.zip", 10)}
// --- downloader.cpp ---#include <iostream>void upload(const char* filename, int timeout = 60); // 声明2:超时默认60秒
void some_other_task() { upload("report.pdf"); // 编译器看到默认值60,实际调用 upload("report.pdf", 60)}
// --- network.cpp --- (真正的函数定义)#include <iostream>void upload(const char* filename, int timeout) { std::cout << "Uploading " << filename << " with timeout " << timeout << "s." << std::endl;}这个语法上允许,但是最好别干。
4. 函数名重载
这个是C++的特性,C语言没有这个东西。
在 C 语言中: 编译器在处理函数时,提供给链接器的“符号名”基本上就是函数名本身。比如你定义了一个函数 void print_int(int x),它在链接器看来可能就是 _print_int。如果你再定义一个同名函数,链接器就会看到两个一模一样的名字,它会立刻报错:“错误:符号 _print_int 被重复定义了!” 它无法区分这两个函数。
在 C++ 语言中: C++ 的编译器非常聪明。它在生成符号名的时候,会把函数的参数类型信息也编码进去。
void print(int i)可能被编译器处理成_print_ivoid print(double d)可能被处理成_print_dvoid print(char c)可能被处理成_print_c
对于链接器来说,_print_i、_print_d 和 _print_c 是三个完全不同的名字!因此它当然不会报错。正是这个底层的“名字修饰”机制,才使得函数重载在 C++ 中成为可能。
这就是函数名重载,也是一种实现多态性的语言机制。
WARNING到这里可以理解多态了。
多态性 (Polymorphism),从字面上看就是“多种形态”。在程序设计中,它的核心思想非常简洁:
允许你使用一个统一的、通用的接口来处理不同类型的数据或对象,而这些数据或对象会根据自身的特性,以不同的方式响应这个接口。
如果两个函数的参数类型和个数相同,只是返回值类型不同,不能重载函数名。
为什么呢?因为仅仅从函数调用本身,编译器有时候根本无法判断程序员到底想用哪个版本。
WARNING确定一个对重载函数的调用对应着哪一个重载函数定义的过程称为绑定(binding),又称定联、联编、捆绑
绑定有静态绑定和动态绑定两种。
- 静态绑定 (Static Binding):在编译时就确定调用哪个函数。重载函数的绑定是静态绑定。
- 动态绑定 (Dynamic Binding):在运行时才确定调用哪个函数。(这个你会在后面学习“多态”和“虚函数”时遇到,现在先不用管它)。
与形参的匹配规则:
- 精确匹配。要类型完全一致,或者经过一些细微转换,比如数组名到第一个元素指针,函数名到指针。
- 提升匹配。
int提到long之类的,float变成double或者long double - 标准匹配:
- 任意算术类型之间可以互换:这是最常见的。比如
int可以换成double,double可以换成int,int可以换成char等。 enum(枚举) 可以换成int等算术类型。- 数字
0可以换成任意指针类型(变成空指针)。 …等等。 - 所有“标准转换”的地位是平等的,没有优先级。
- 任意算术类型之间可以互换:这是最常见的。比如
因为没有优先级,所以有多个转换选项的时候,就会报错。
比如
void print(char c);void print(double d);
// 现在调用print(1);最后,编译器还有绝招:自定义匹配。如果都失败了,编译器会使出最后的绝招:它会去检查参数的类型本身(也就是类),看看这个类有没有提供一个“转换说明书”,能把自己变成函数需要的类型。
什么是“自定义转换”?
简单来说,就是我们自己写的类,可以主动告诉编译器如何将自己转换成其他类型。
“人民币” (class Money)。
class Money {public: Money(double amount) : value(amount) {} double getValue() const { return value; }private: double value;};
void printDouble(double d) { std::cout << "Value: " << d << std::endl;}
Money myWallet(100.5);printDouble(myWallet); // 这行代码会报错!编译器会报错,因为它执行了 Plan A, B, C 后发现,printDouble 需要一个 double,而你给了一个 Money 对象。它不知道 Money 和 double 是什么关系。
如果我们增加一个自定义转换:
class Money {public: Money(double amount) : value(amount) {} double getValue() const { return value; }
// 它告诉编译器:如果你需要一个 double,而手上只有一个 Money 对象, operator double() const { return this->value; }
private: double value;};