Lesson27 函数进阶
27.1 函数的默认参数
在 C++ 中,函数的参数表中的形参是可以有默认值的。比如:
1 | int func(int a, int b = 10); |
在使用默认参数 (Default Argument) 时,我们需要注意以下几点:
-
调用函数时,任何明确提供的参数必须按参数表顺序(从左往右)输入(不能跳过具有默认值的参数)。对于有默认值的参数,用户调用时未提供数据则按默认值传入;有提供数据则按用户输入传入。
1
2
3
4
5
6
7
8
9
10
11
12
13void print(int a = 20, double d = 10.0){
printf("%d\n", a);
printf("%f\n", b);
}
int main(void)
{
print(); // okay: both arguments defaulted
print(60); // okay: d defaults to 10.0
print(10.0); // error: does not match above function (cannot skip argument for a)
return 0;
} -
如果一个参数被赋予默认参数,那么所有后续的参数(右侧的)也必须是默认参数。因此,建议在函数末尾定义默认参数。
1
2//void print(int x=10, int y); // not allowed
void print(int x = 10, int y = 10);// right -
一旦声明,默认参数就不能在同一源文件中重新声明。这意味着对于同时具有声明和函数定义的函数,默认参数可以在声明或函数定义中声明,但不能同时声明。
1
2
3
4
5
6
7
8
9
void print(int x, int y=4); // forward declaration
void print(int x, int y=4) // compile error: redefinition of default argument
{
printf("%d\n", x);
printf("%d\n", y);
}默认参数必须在源文件中开头声明后才能使用:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void print(int x, int y); // forward declaration, no default argument
int main()
{
print(3); // compile error: default argument for y hasn't been defined yet
return 0;
}
void print(int x, int y=4)
{
printf("%d\n", x);
printf("%d\n", y);
}Tip我们建议在函数声明而非定义时设置默认参数。
-
建议:如果有多个默认参数,则最左侧的参数应该是用户最可能明确设置的参数。
在函数调用中,有些参数并不常用,因此默认值可以满足要求。默认参数可以突出对于函数具有重大意义的参数。
——Default Arguments | Microsoft Learn
Default arguments are an excellent option when a function needs a value that has a reasonable default value, but for which you want to let the caller override (覆盖) if they wish.
——11.5 — Default arguments – Learn C++
需要时,用户也可以覆盖默认参数。
27.2 函数的占位参数
在 C++ 中,函数的参数列表可以有占位参数 (Placeholder Argument),只使用类型名而不提供变量名,用以占位。比如:
1 | int func(int a, int);//后一个参数就是占位参数 |
调用函数时,未指定数据的情况下,占位参数必须提供。一般情况下,在函数体内部无法使用占位参数,因为没有可以访问其位置的事物(变量名)。
1 |
|
占位参数也有默认值。此时可按照默认参数规则,不提供占位参数:
1 | int print(int a, int = 10){ |
占位参数常见的用途包括:
- 与旧代码兼容:当修改函数签名时,为了保持与旧代码的兼容性,可以保留不再使用的参数。
- 占位:为将来可能增加的参数预留位置。
- 匹配特定函数签名:在某些情况下,可能需要一个特定的函数签名,比如使用回调函数时。此时可以通过占位参数来满足签名的要求。
27.3 函数重载
27.3.1 函数重载的概念
考虑这个函数:
1 | int add(int x, int y){ |
add
这个函数用来计算两个整数的和。但如果我们需要计算两个浮点数的和呢?我们不可能直接使用 add
,因为参数表的类型根本不匹配!于是我们又写了一个函数:
1 | double addDouble(double x, double y){ |
挺好的,只要你能在写了成百上千行代码再放了几天假期后继续工作时还能记起来整数加法用 add
、浮点数加法用 addDouble
就行。
However, for best effect, this requires that you define a consistent function naming standard for similar functions that have parameters of different types, remember the names of these functions, and actually call the correct one.
And then what happens when we want to have a similar function that adds 3 integers instead of 2? Managing unique names for each function quickly becomes burdensome (繁琐).
——11.1 — Introduction to function overloading – Learn C++
太繁琐、太易错、太不优雅了!
所以,既然都是加法,为什么还要分成整数加法和浮点数加法呢?能不能统一成一个加法函数呢?
C++ 提供了一种解决方法,我们称为函数重载 (Function Overloading)。函数重载允许我们创建多个名称相同的函数,只要每个同名函数具有不同的参数类型(或者函数可以通过其他方式区分)。在相同的作用域中共享名称的每个函数都称为重载函数(有时简称为重载)。
重载函数有如下条件:
- 同一作用域
- 函数名称相同
- 函数参数表不同:类型不同、数量不同、(类型) 顺序不同
Functions can be overloaded so long as each overloaded function can be differentiated (区分) by the compiler. If an overloaded function can not be differentiated, a compile error will result.
编译器会区分重载,区分不来就会报错。
The table below shows how overloaded functions are differentiated:
Function property | Used for differentiation | Notes |
---|---|---|
Number of parameters | Yes | |
Type of parameters | Yes | Excludes typedefs, type aliases, and const qualifier on value parameters. Includes ellipses (…, acts as a wildcard (通配符) which matches any actual argument). |
Return type | No |
——11.1 — Introduction to function overloading – Learn C++&11.2 — Function overload differentiation – Learn C++
需要注意类型别名和 const
修饰的按值传递的参数无法用于区分重载。
Note that a function cannot be overloaded only by its return type. At least one of its parameters must have a different type.
——Overloads and templates - cplusplus.com
函数返回值不同不能作为函数重载的条件。
(变量名称更不行哈)
Consider the case where you want to write a function that returns a random number, but you need a version that will return an int
, and another version that will return a double
. You might be tempted to do this:
1 | int getRandomValue(); |
But If you were the compiler, and you saw this statement:
1 | getRandomValue(); |
Which of the two overloaded functions would you call? It’s not clear.
The best way to address this is to give the functions different names:
1 | int getRandomInt(); |
——11.2 — Function overload differentiation – Learn C++
如果两个函数仅有返回值不同,那么调用时编译器就无法区分两个函数了。
下面进行演示:
1 | int add(int x, int y) // integer version |
1 | int operate (int a, int b) |
Two functions with the same name are generally expected to have -at least- a similar behavior, but this example demonstrates that is entirely possible for them not to. Two overloaded functions (i.e., two functions with the same name) have entirely different definitions; they are, for all purposes, different functions, that only happen to have the same name.
——Overloads and templates - cplusplus.com
一般具有相同名称的函数应该具有相近的功能,但 C++ 也允许定义功能不同的重载函数(毕竟返回值不作为确认重载的条件)。
为了编译一个重载函数的调用,编译器需要进行重载解析 (Overload resolution) 以确定调用的是哪个重载函数。简单来说,与调用时函数参数表最为接近的重载将被调用:
1 | void f(long); |
(Overload resolution - cppreference.com)
对于有多个参数的重载,编译器会选择与函数调用时提供的参数表匹配得最好的那个重载:
1 |
|
main
函数中调用的 print
将匹配到 void print(char, int)
。因为函数调用时,第一个参数'x'
均可以匹配到三个重载的第一个参数 char
;对于第二个参数'a'
,由于字符类型是一个特殊的整数,所以它和 int
的匹配度是最好的,剩余两个需要转换。所以编译器选择调用匹配度相对最好的 void print(char, int)
所选函数必须至少在一个参数上提供比所有其他候选函数更好的匹配,而在所有其他参数上则不能更差。
——11.3 — Function overload resolution and ambiguous matches – Learn C++
27.3.2 引用作为重载参数
使用引用作为参数的函数也可以重载,但有几个注意事项需要注意:
-
编译器无法区分变量及其引用。
1
2
3
4
5
6
7
8
9
10int add(int a, int b){
return a + b;
}
/*
// error: same argument list
int add(int& a, int& b){
return a + b;
}
*/Overloaded functions differentiate between argument types that take different initializers. Therefore, an argument of a given type and a reference to that type are considered the same for the purposes of overloading. They’re considered the same because they take the same initializers. For example,
max( double, double )
is considered the same asmax( double &, double & )
. Declaring two such functions causes an error. -
编译器可以区分一般引用和由
const
修饰的引用。1
2
3
4
5
6
7
8int add(int& a, int& b){
return a + b;
}
// okay: the two have different argument list
int add(const int& a, const int& b){
return a + b;
}
27.3.3 默认参数能不能用于重载?
我们先不去调用函数:
1 |
|
编译通过!在编译器看来,这两个函数属于重载。
那么我们调用一下函数试试?
1 |
|
好了,VS2022 的 Intellisense 很快啊,输出了一个错误信息:
1 | 错误(活动) E0308 有多个重载函数 "print" 实例与参数列表匹配 12 |
编译器也提示:
1 | error C2668: “print”: 对重载函数的调用不明确 |
当要重载的函数出现默认参数时,可能造成二义性,导致报错,需要避免。
调用时,默认参数不能作为区分重载的条件。仅有默认参数不同的两个函数将被认为是重复定义,而不是重载。
——Function Overloading | Microsoft Learn
如果有多个同优先级的匹配可能,编译器会停止匹配,并报告关于模糊匹配的错误。(如正文提供的编译器错误信息)
——11.3 — Function overload resolution and ambiguous matches – Learn C++
Lesson28 类与对象 - Part1
“类” 是 C++ 一个重要的概念,以至于 C++ 的原型就是 “C with class”(带类的 C)。
The central language feature of C++ is the class.
——A Tour of C++, third edition
28.0 面向对象编程
C++ 是一个面向对象 (Object-oriented) 的语言。它遵循面向对象编程 (Object-oriented programming, OOP) 的编程范式,以对象为中心设计编程语言。我们主要关注三个最重要的概念:封装 (Encapsulation)、继承 (Inheritance) 和多态 (Polymorphism)
有些文章会有其他的表述,例如 Microsoft 认为 OOP 还有一个抽象 (Abstraction) 特征。
与 C++ 不同的是,C 语言是一个典型的过程式 (Procedural) 语言。它关注的是过程,是 “What should the program do next?”(MIT 6.096),造成了复杂性和不可维护性。OOP 就是解决这种复杂性的一种方案。
The original programming paradigm (范式) is: Decide which procedures you want; use the best algorithms you can find.
The focus is on the processing – the algorithm needed to perform the desired computation. Languages support this paradigm by providing facilities for passing arguments to functions and returning values from functions. The literature related to this way of thinking is filled with discussion of ways to pass arguments, ways to distinguish different kinds of arguments, different kinds of functions (e.g., procedures, routines, and macros), etc.
在面向对象编程中,所有的实例都是对象,拥有其属性和行为。具有相同性质的对象,可以抽象为类 (Class)。
注意这里的 “对象” 和我们在 24.3.SP 所说的数据 “对象” 不一样。
In programming, properties are represented by objects, and behaviors are represented by functions.
In object-oriented programming (often abbreviated as OOP), the focus is on creating program-defined data types that contain both properties and a set of well-defined behaviors. The term “object” in OOP refers to the objects that we can instantiate (实例化,具象化) from such types.
Note that the term “object” is overloaded a bit, and this causes some amount of confusion. In traditional programming, an object is a piece of memory to store values. And that’s it. In object-oriented programming, an “object” implies that it is both an object in the traditional programming sense, and that it combines both properties and behaviors. We will favor the traditional meaning of the term object in these tutorials, and prefer the term “class object” when specifically referring to OOP objects.
——14.1 — Introduction to object-oriented programming – Learn C++
在编程中,事物的属性由数据对象表示,行为由函数表示。
OOP 的重点在于创造拥有一组属性和行为的数据类型,其所指的 “对象” 是指具象化这些类型后得到的数据对象。
之后的教程中,OOP 中的 “对象” 将被称为 “类对象”,以区分传统编程意义上的 “对象”。
28.1 封装
28.1.1 封装的意义
宽泛地讲,封装就是把一些数据和信息打包进一个单元里面。在 C++ 中,封装特征的体现就是类。类是一组属性和行为的整合。

(来源:Encapsulation in C++ - GeeksforGeeks)
类是一个用户定义类型,可以用来表达程序源码中的某个概念,这样我们可以直接在代码中知道如何使用这个概念,而不需要依靠其他资料,比如设计文档、评论还有开发者的头脑。由一组精挑细选的类构建的程序比直接根据内置类型构建所有内容的程序更容易理解和正确执行。(就是不要重复造轮子)
一般来说,头文件中提供的很多东西就是类。
——A Tour of C++, third edition
我们先来写一个类吧。
如果需要定义一个类,我们需要:
1 | class name { |
例如,我们希望设计一个 “圆” 类,属性为半径,行为为求该圆周长,则:
1 | class circle { |
我们现在已经创建了一个类,对应一个抽象的 “圆”。但我们还需要一个具体的实例来运用这些类,所以接下来我们创建一个类对象,并赋予属性:
1 | circle r1; |
然后输出这个圆的周长:
1 | printf("%.2f\n", r1.calcCC()); |
运行一下:
1 | 31.40 |
学生类设计展示:
1 | class student { |
1 |
|
这里有一些 C++ 的新语法没有介绍,可以参考:5.10 — Introduction to std::string – Learn C++;
<iostream>
| Microsoft Learn
我们可以也可以利用类中的函数为类中的属性赋值:
1 | class student { |
类中的函数可以在类外定义,定义时需要附上该函数的命名空间(可以理解为是类的作用域):
1 | class student { |
类中的属性和行为统称为成员 (Member)。其中,属性称为 “成员属性” 或 “成员变量”;行为称为 “成员函数” 或 “成员方法”。
考虑命名数据成员时以 “m_” 前缀开头,从而与局部变量、函数参数和成员函数的名称形成区别。
——14.5 — Public and private members and access specifiers – Learn C++
28.1.2 类的访问权限
公园是公共场所,而住宅是私人的。你和其他人可以合法自由地进出公共场所,但只有住宅的成员以及被明确允许进入的人才能进入私人住宅。
——14.5 — Public and private members and access specifiers – Learn C++
类的一大特色就在于它的 “数据保护” 属性。通过为类成员设置不同的访问权限,我们得以保证数据操作是受控且安全的。
C++ 提供了三级访问权限:
public
:类内成员可以访问,类外也可以访问。protected
:类内成员可以访问,类外不能访问。该类的派生类也可以访问protected
成员private
:类内成员可以访问,类外不能访问。派生类不能访问private
成员
访问控制基于类级别(而非对象级别)。这意味着一个类的成员函数可以访问同一类型的任何类对象的私有成员。
每次访问成员时,编译器都会检查该成员的访问级别。如果访问行为不被允许,编译器将生成编译错误。
1 | class info { |
不指定访问权限时,类内成员默认为 **private
成员 **。
我们定义的类的访问权限是给编译器看的。在运行时程序不会检查类的访问权限合法与否。
我们建议,将成员变量的访问权限设为 private
(或 protected
),成员函数的访问权限设为 public
。这被称为信息隐藏 (Information Hiding)。
这样做有以下好处:
-
自主可控的读写权限管理。
对于可读可写的成员变量,外界可以通过成员函数来写入与读取;对于只读的成员变量,我们可以不提供写入的成员函数;对于只写的成员变量,我们可以不提供读取的成员函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23// r:read w:write
class info {
private:
string m_name;// rw
int m_card = 3586286477362;// r
string m_Idol;// w
public:
// rw
void SetName(string name){
m_name = name;
}
string GetName(){
cout << m_name << '\n' << endl;
}
// r
int GetCard(){
cout << m_card << '\n' <<endl;
}
// w
void SetIdol(string idol){
m_Idol = idol;
}
}; -
写入数据前的有效性检查。
如果对输入的数据有要求,可以在写入时进行检查。
1
2
3
4
5
6
7
8
9
10
11
12class info{
private:
int m_age;
public:
void SetAge(int age){
if (age < 0 || age > 150){
cout << "Invalid input, please retry.\n" << endl;
return;//to end the function
}
m_age = age;
}
};
根据上面的原则,如果我们得到了一个类,我们只需要考虑它对外能做什么,而不必追究它内部的实现。前者就是我们所说的接口。
28.1.3 类和结构体的区别
学到这里是不是有一种既视感?我们在 C++ 写的类和在 C 语言写的结构体有十分甚至九分的相像,甚至共用一些名词!
从技术角度来看,结构体和类几乎相同。
在 C++ 中,类和结构体唯一的不同就在于其成员默认的访问权限不同。
类内成员的访问权限默认为 private
,而结构体内成员的访问权限默认为 public
。
1 | struct st { |
28.1.SP 类设计练习
SP.1 设计立方体类
设计目标:
- 设计一个立方体 (Cube) 类,求出立方体的面积和体积
- 分别用全局函数和成员函数判断两个立方体是否相等
样例:
1 | class cube { |
用法:
1 |
|
SP.2 设计点圆类
设计目标:
-
设计一个圆 (crircle) 类和一个点 (point) 类
-
判断圆和点的几何关系
点 和 的位置关系有三种: 在 外; 在 内; 在 上。
设 到圆心 的距离为 , 的半径为 ,则:
- 在 外 .
- 在 内 .
- 在 上 .
(来源:圆中的位置关系 | whk wiki)
样例:
1 | class circle { |
用法:
1 |
|