6.函数
典型的函数包含以下部分:返回类型、函数名字、0或多个形参(parameter)组成的列表以及函数体。
通过调用运算符( )来执行函数,圆括号内是用逗号隔开的实参(argument)列表。
形参和实参有对应关系,但是没有规定实参的求值顺序,编译器能以任意可行的顺序对实参求值。在调用时也会发生隐式格式转换。
自动对象:只存在于块执行期间的对象。当块执行结束后,块中创建的自动对象的值就变成未定义的了。形参也是一种自动对象。
局部静态对象:将局部变量定义成 static 类型,使其生命周期贯穿函数调用及之后的时间。例如统计函数自己被调用了多少次:
size_t count_calls(){
static size_t ctr = 0;
return ++ctr;
}
int main(){
for(size_t i = 0; i != 10; ++i){
cout<< count_calls() << endl;
}
return 0;
}
局部静态对象在程序的执行路径第一次经过对象定义语句时初始化,并且直到程序终止才被销毁。
函数声明:也称为函数原型。建议在头文件中声明,在源文件中定义,含有函数声明的头文件应该被包含到定义函数的源文件中。
!!在传引用参数时,应避免拷贝。例如比较两个非常长的 string 大小时,由于无需改变 string 的内容,所以最好把形参定义成对常量的引用(即常量引用):
bool isShorter(const string s1, const string s2)
{
return s1.size() < s2.size();
}
如果需要多个返回值的时候,可以通过引用传递需要的值进来进行改变。
-
const形参和实参
和其他初始化过程一样,当用实参初始化形参时,会忽略掉顶层 const 。当形参有顶层 const 时,传给它常量对象或者非常量对象都是可以的。
void fcn(const int i) {...} //fcn能读取i,但是不能向i写值 // 调用fcn时,既可以输入 const int,也可以传入 int
重复定义相同名字的函数时,形参必须要有明显不同,如果再定义一个
void fcn(int i)
是错误的,这两个形参没有什么不同。参数传递中的引用必须用同类型的对象初始化。
!!!尽量使用常量引用:
例如函数
find_char
,有string::size_type find_char(string &s, char c, string::type &occurs) {...}
在用如下方式调用时会报错:
find_char("Hello world", 'o', ctr);
第一个形参的类型应该定义成
const string&
,就可以同时接受const string
和string
了。 -
数组形参
数组有两个性质:不允许拷贝数组,数组使用时通常会转换成指针。
但是仍然可以把形参写成数组形式,下面3个等价:
void print(const int*); void print(const int[]); void print(const int[10]); // 期望数组含有10个元素,实际不一定
对编译器而言都是传入了 const int* ,使用时必须确保数组不越界!!
int i = 0, j[2] = {0, 1}; print(&i); // 正确,&i的类型是 int* print(j); // 正确,j转换成 int* 并指向 j[0]
要管理数组实参,使用传递指向数组首元素和尾后元素的指针,标准库规范。
void print(const int *beg, const int *end) { while(beg != end) cout << *beg++ <
当不需要对数组元素进行改变的时候,数组形参应该是指向 const 的指针。
形参也可以是数组的引用,但是也同时限制了函数的可用性,只能用于对应长度的数组上。
// 只能作用于大小为 10 的数组 void print(int (&arr)[10]) // 括号不能少,不然就变成了引用的数组,int& [] { for(auto elem : arr) cout << elem <
!! 传递多维数组形参:
真正传递的是指向数组首元素的指针,数组第二维开始的维度都是数组元素的一部分。
void print(int (*matrix)[10], int rowSize) // (*matrix) 括号必不可少!!! {...} // 也可以如下写法,编译器依旧会忽略掉第一个维度 void print(int matrix[][10], int rowSize) {...}
-
main:预处理命令行选项
int main(int argc, char *argv[]) {...}
第二个形参 argv 是一个数组,元素是指向C风格字符串的指针;第一个形参 argc 表示的是数组中字符串的数量。或者可以写成
char **argv
例如命令行: prog -d -o ofile data0 有 argc = 5
argv[0] = "prog"; // argv[0] 保存程序的名字,而非用户输入 argv[1] = "-d"; argv[2] = "-o"; argv[3] = "ofile"; argv[4] = "data0"; argv[5] = 0;
-
返回值
两种形式:
return; return expression;
返回值为 void 类型可以不写
return;
函数结束时最后也会隐式地调用这句话。!! 在含有返回值的函数中,若最后是一个循环体,在循环体内有 return ,循环体后也应该有一条 return 语句,编译器可能发现不了这个错误。
调用运算符的优先级与点运算符和箭头运算符相同,并且也符合左结合律。
调用一个返回值为引用类型的函数得到左值,其他返回右值。
因此可以给返回值为非常量引用的函数的结果赋值。
返回值还可以是由列表初始化:
vector
process(){ ... if(A.empty) return {}; // 返回一个空 vector 对象 else if(...) return {"functionX", "okay"}; // 返回一个列表初始化的 vector 对象 else return {"functionX", "okay", A}; } !!返回数组指针的函数,因为无法返回数组,所以只能返回数组的指针:
采用类型别名的方法:
using arrT = int[10]; arrT* func(int i);
不使用别名的方法:
int (*func(int i))[10]; // 括号必不可少!! // 这和声明一个指向 10 个整数的数组的指针类似 int arr[10]; int (*p)[10] = &arr;
func(int i)
表示调用 func 函数时需要一个 int 类型的实参(*func(int i))
意味着我们可以对函数调用的结果执行解引用操作(*func(int i))[10]
表示解引用 func 的调用将得到一个大小是 10 的数组int (*func(int i))[10]
表示数组中的元素是 int 类型
-
main 函数的返回值
main 函数允许没有返回值,会默认添加一个返回0的return语句。
main 函数的返回值可以看作是状态指示器,返回0表示执行成功,返回其他值表示执行失败,其中非0值的具体含义依机器而定。
cstdlib 头文件中定义了两个预处理变量(不能在它们前面加上std::,也不能在using声明中出现),分别表示成功和失败:
if (...) return EXIT_FAILURE; else return EXIT_SUCCESS;
函数重载
-
重载和 const 形参
顶层 const 不影响传入函数的对象,一个拥有顶层 const 的形参无法和另一个没有顶层 const 的形参区分开来。
无法实现重载:
Record lookup(Phone); Record lookup(const Phone); // 重复的声明
而对于形参是某种类型的指针或引用,则通过区分其指向的是常量对象还是非常量对象可以实现函数重载,此时 const 是底层的。
Record lookup(Account&); // 函数作用于 Account 的引用 Record lookup(const Account&); // 新函数,作用于常量引用 Record lookup(Account*); // 函数作用于指向 Account 的指针 Record lookup(const Account*); // 新函数,作用于指向常量的指针
编译器会优先选择非常量的版本!!
-
const_cast 和重载
const_cast 去掉const性质,将常量对象转化为非常量。(如果对象本身不是常量,则可以进行写操作)
例如返回一个较短的 string:
const string &shorterString(const string &s1, const string &s2) { return s1.size() <= s2.size() ? s1 : s2; } // 这个函数返回的仍然是一个 const string 的引用 // 可以对两个非常量调用这个函数,但返回值仍然是个 常量 string &shorterString(const string &s1, const string &s2) { auto &r = shorterString(const_cast
(s1), const_cast (s2)); return const_cast (r); }
特殊用途语言特性
-
默认实参
一旦某个形参被赋予了默认值,它后面的所有形参都必须有默认值。
在调用时,通过省略实参的方法调用。
!! 注意只能省略尾部的实参,不能中间省略一个后面继续写值。
默认实参的声明:
// 原函数: string screen(int ht = 24, int wd = 80, char background = ' '); // 假如已有如下声明: string screen(int, int, char = ' '); // 我们不能修改一个已经存在的默认值,如: string screen(int, int, char = '*'); // 可以按如下形式添加默认实参: string screen(int = 24, int = 80, char);
-
内联函数和constexpr函数
内联函数:在函数前添加 inline ,可以避免函数调用开销。
一般不支持内联递归调用,太复杂的函数也不行。
constexpr函数:是指能用于常量表达式的函数。
函数匹配
第一步:选定本次调用对应的重载函数集,称为候选函数。候选函数具备两个特征:一是与被调用的函数同名,二是其声明在调用点可见。
第二步:考察本次调用的实参,选出可行函数。可行函数的两个特征:一是形参数量和本次调用的实参数量相等,二是每个实参的类型与对应的形参类型相同,或者能转换。(如果有默认值,传入的实参可能少于它实际使用的实参数量)
-
第三步:寻找最佳匹配。一般认为精确匹配比需要类型转换的好。在含有多个形参进行匹配时,满足以下条件则匹配成功:
- 该函数每个实参的匹配都不劣于其他可行函数需要的匹配
- 至少有一个实参的匹配优于其他可行函数提供的匹配
对于实参:(42, 2.56); 如果用 (int, int)
和 (double, double)
两种重载,对于第一个实参来说,int更好,第二个实参来说,double更好,这个调用具有二义性,编译器会拒绝其请求。
函数指针
指向的是函数,而非对象!
函数的类型由它的返回类型和形参类型共同决定,和函数名无关。例如函数:
bool lengthCompare(const string&, const string&);
声明一个可以指向该函数的指针:
bool (*pf)(const string&, const string&); // 未初始化
// 括号必不可少!否则会变成pf是一个返回值为bool*的函数:
bool *pf(const string&, const string&);
给函数指针赋值:
pf = lengthCompare;
pf = &lengthCompare; // 两者等价,取地址符是可选的
直接使用指向函数的指针调用该函数,无需解引用
bool b1 = pf("hello", "world");
bool b2 = (*pf)("hello", "world"); // 等价调用
函数指针可以赋值一个nullptr,或者值为0的整型表达式。
函数指针可以作为形参使用,但是名字较为复杂,最好使用类型别名的方式:
typedef decltype(lengthCompare) Func2; // 函数类型 Func2
typedef decltype(lengthCompare) *FuncP2; // 指向函数的指针
//或者:
typedef bool (*FuncP1)(const string&, const string&); // FuncP1和FuncP2一样
void useBigger(const string&, const string&, FuncP2); // 两者等价
void useBigger(const string&, const string&,
bool pf(const string&, const string&));
返回指向函数的指针:
using F = int(int*, int); // F是函数类型
using PF = int(*)(int*, int); // PF是指针类型,括号!!
PF f1(int); // PF是指向函数的指针,f1返回指向函数的指针
F *f1(int); // 显示地指定返回类型是指向函数的指针,返回值必须加*号!!
// 等价于
int (*f1(int))(int*, int);
7.类
类的基本思想是数据抽象和封装,数据抽象是一种依赖于接口和实现分离的编程技术。类的接口包括用户所能执行的操作;类的实现则包括类的数据成员、负责接口实现的函数体以及定义类所需的各种私有函数。
类的本身就是一个作用域,成员函数的定义嵌套在类的作用域内。
编译器编译时,首先编译成员的声明,然后才轮到成员函数体,因此成员函数体可以随意使用类里的其他成员而无需在意这些成员出现的次序。
常量成员函数:把const关键字放在成员函数的参数列表之后,表示this是一个指向常量的指针。
double Sales_data::avg_price() const {
if(units_sold)
return revenue/units_sold;
else
return 0;
}
构造函数
构造函数的任务是初始化对象的数据成员,只要类的对象被创建,就会执行构造函数。
特点:没有返回类型;类似于重载,可以有多个构造函数;不能被声明成const,因为直到构造函数完成初始化过程,对象才真正取得其“常量”属性。
如果不写构造函数,则会有一个默认构造函数(又称为合成的默认构造函数),初始化规则:
- 如果存在类内的初始值,用它来初始化成员
- 否则,默认初始化成员
如果一个类包含有内置类型或复合类型的成员,只有当这些成员全部都被赋予了类内的初始值时,这个类才适合于使用合成的默认构造函数!!
如果需要这个构造函数使用合成的默认构造函数,则:
Sales_data() = default; // 加上 = default
构造函数初始值列表,只为数据成员赋值:
Sales_data(const string &s): bookNo(s), units_sold(0), revenue(0) { } // 函数体为空,只为数据成员赋初值
!!成员的初始化顺序与他们在类定义中的出现顺序一致,和构造函数中的顺序无关。(最好让这两个顺序一致,并且避免用某些成员初始化其他成员)
委托构造函数:使用它所属类的其他构造函数执行它自己的初始化过程,或者说把它自己的一些(或全部)职责委托给了其他构造函数。
// 非委托构造函数
Sales_data(string s, unsigned cnt, double price):
bookNo(s), units_sold(cnt), revenue(cnt*price)
// 委托构造函数
Sales_data() : Sales_data(" ", 0, 0) {}
Sales_data(s) : Sales_data(s, 0, 0) {}
explicit构造函数:只能用于值初始化,不允许类类型转换!!
class Sales_data{
public:
explicit Sales_data(istream&);
};
聚合类:用户可以直接访问其成员,并且具有特殊的初始化语法形式,当一个类满足如下条件时,可以说它是聚合的:
- 所有成员都是 public 的
- 没有定义任何构造函数
- 没有类内初始值
- 没有基类,也没有 virtual 函数
可以用一个花括号括起来的列表初始化聚合类:
struct Data{
int ival;
string s;
};
Data val1 = {0, "Anna"}; // 初始值顺序要和声明的顺序一致!
访问控制与封装
使用访问说明符加强类的封装性:
- public说明符之后的成员在整个程序内可被访问,public成员定义类的接口。
- private说明符之后的成员可以被类的成员函数访问,但不能被使用类的代码访问,private部分封装了类的实现细节。
!! class 和 struct 唯一的区别就是默认访问权限,struct中,第一个访问说明符前的成员是 public;而 class 关键字下,这些成员是 private 的。
如果我们希望定义的类的所有成员是 public 时,用 struct;反之如果我们希望成员是 private 的,使用 class
友元:类可以允许其他类或者函数访问它的非公有成员,方法是令其他类或者函数成为它的友元(friend)。
封装的两个重要优点:
- 确保用户代码不会无意间破坏封装对象的状态
- 被封装的类的具体实现细节可以随时改变,而无需调整用户级别的代码
友元
类外的类或者函数要访问类的非公有成员,需要令类或者函数称为他的友元,添加一条以 friend 关键字开始的函数声明语句:
class Sales_data{
//友元声明
friend Sales_data add(const Sales_data&, const Sales_data&);
public:
...
private:
...
};
Sales_data add(const Sales_data&, const Sales_data&); //非成员组成部分声明
友元类:可以访问此类包括非公有成员在内的所有成员。
class Screen{
friend class Window_mgr;
// 剩余部分
};
友元不具有传递性!每个类负责控制自己的友元类和友元函数。
令成员函数成为友元:
class Screen{
friend void Window_mgr::clear(ScreenIndex);
};
!!要注意声明和定义的关系,例如上述的 Screen 和 Window_mgr 类之间:
- 首先定义 Window_mgr 类,其中声明 clear 函数,但不能定义它。在 clear 使用 Screen 的成员之前必须先声明 Screen。
- 接下来定义 Screen,包括对于 clear 的友元声明。
- 最后定义 clear , 此时它才可以使用 Screen 的成员。
!! 重载函数每个都要进行友元声明
其他特性
可变数据成员: mutable,一个可变数据成员永远不会是const,即使它是const对象的成员。例如在 const 成员函数中改变一个可变成员:
class Screen{
public:
void some_member() const;
private:
mutable size_t access_ctr;
};
void Screen::some_member() const {
++access_ctr; // 用于记录成员函数被调用的次数
}
类内初始值的声明:必须使用 = 的形式,或者花括号括起来的直接初始化形式:
vector screens{Screen(24,80,' ')}; // 总是有一个默认初始化的空白 Screen
对于返回 *this
的成员函数,例如 move(..)
和 set(..)
返回值都是 Screen&
,即 return *this;
那么就可以把一系列操作连接在一条表达式中:
myScreen.move(4, 0).set('#');
如果返回值不是 Screen& 而是 Screen,则不能进行上述调用,
Screen temp = myScreen.move(4, 0);
temp.set('#');
同时,如果新加一个打印函数 display,为 const 成员,返回的 *this 指针是 const 对象,如下调用会出错:
Screen myScreen;
myScreen.display(cout).set('*'); // set 的调用无法通过编译
!! 一个const成员函数如果以引用的形式返回 *this, 那么它的返回类型将是常量引用。
类的声明也可以和定义分离开:
class Screen; // 仅声明而不定义
这种声明称为前向声明,向程序中引入了名字 Screen 并指明它是一种类型,在定义之前, Screen 是一个不完全类型。
类的作用域
牢记一个类就是一个作用域。
返回类型出现在类名之前,因此它是位于作用域之外的,要指定哪个类定义了它:
Window_mgr::ScreenIndex Window_mgr::addScreen(const Screen &s)
{
screen.push_back(s);
return screen.size() - 1;
}
静态成员
类的一些成员与类本身直接相关,而不是与类的各个对象保持关联。
类的静态成员可以是 public 的或 private 的,可以是常量、引用、指针、类类型等。类的静态成员存在于任何对象之外,对象中不包含任何与静态数据成员有关的数据!!被所有同样的类共享。
静态成员函数不与任何对象绑定在一起,不包含 this 指针,不能声明成 const 的,也不能在 static 函数体内使用 this 指针。