原书:Essential C++, Stanley B. Lippman, 电子工业出版社, 2013.
章节:第2章 面向过程的编程风格
环境:CLion + MinGW
2.1 如何编写函数
-
函数的四个组成部分:返回类型、函数名、参数列表、函数体
-
函数声明与函数定义
(1) 函数声明必须指明返回类型、函数名、参数列表,但不必提供函数体,即所谓的函数原型。函数声明让编译器得以检查后续出现的使用方式是否正确——是否有足够的参数、参数类型是否正确等
(2) 函数定义包括函数原型和函数体
-
返回Fibonacci数列第n个元素的函数
#include <iostream> #include <cstdlib> // 书中说使用exit函数应包含cstdlib头文件,但在当前环境下不包含也可使用 using namespace std; bool fibonacci_elem(int index, int* elem); // 声明fibonacci_elem函数 int main() { int index; cin >> index; cout << fibonacci_elem(index) << endl; return 0; } int fibonacci_elem(int index){ if(index <= 0){ // 用户输入不合理索引时,调用exit传入一个值,其为程序结束时的状态值 cerr << "Your should enter a postive integer." << endl; exit(-1); } int elem = 1; int n1 = 1, n2 = 1; for(int i = 3; i <= index; ++i){ elem = n1 + n2; n1 = n2; n2 = elem; } return elem; }
(1) 使用exit函数处理用户的不合理输入,如上述代码所示
(2) 抛出异常(exception),表示函数收到了错误的索引值
(3) 返回0,其不在Fibonacci序列中,提示用户异常,但此方式不好
(4) 改变fibonacci_elem函数返回值,使其能表示能否计算出用户想要的值
1) 刚开始我是这样写的,使用指针改变变量值
#include <iostream> using namespace std; bool fibonacci_elem(int index, int* elem); // 声明fibonacci_elem函数 // bool fibonacci_elem(int, int*); // 函数声明可以省略参数名称,只指明参数类型 int main() { int index, elem = 0; cin >> index; if(fibonacci_elem(index, &elem)) cout << elem << endl; return 0; } bool fibonacci_elem(int index, int* elem){ const int max_index = 45; // 限制最大索引值,否则结果可能会溢出 if(index <= 0 || index > max_index){ cerr << "Your should enter a positive integer no more than " << max_index << endl; return false; // 返回值为布尔类型,表示能否计算得到指定索引的Fibonacci序列元素 } int ans = 1; int n1 = 1, n2 = 1; for(int i = 3; i <= index; ++i){ ans = n1 + n2; n1 = n2; n2 = ans; } *elem = ans; // 通过指针返回对应的Fibonacci序列元素值 return true; }
2) 书中是这样写的,使用引用改变变量值
#include <iostream> using namespace std; bool fibonacci_elem(int, int &); // 函数声明可以省略参数名称,只指明参数类型 int main() { int index, elem = 0; cin >> index; if(fibonacci_elem(index, elem)) cout << elem << endl; return 0; } bool fibonacci_elem(int index, int &elem){ const int max_index = 45; // 限制最大索引值,否则结果可能会溢出 if(index <= 0 || index > max_index){ cerr << "Your should enter a positive integer no more than " << max_index << endl; return false; // 返回值为布尔类型,表示能否计算得到指定索引的Fibonacci序列元素 } int ans = 1; int n1 = 1, n2 = 1; for(int i = 3; i <= index; ++i){ ans = n1 + n2; n1 = n2; n2 = ans; } elem = ans; // 通过引用将变量设为对应的Fibonacci序列元素值 return true; }
-
打印Fibonacci数列的前n个元素的函数
#include <iostream> #include <iomanip> using namespace std; bool print_fibonacci(int); int main() { int index; cin >> index; print_fibonacci(index); return 0; } bool print_fibonacci(int index) { const int max_index = 45; if(index <= 0 || index > max_index){ cerr << "Your should enter a positive integer no more than " << max_index << endl; return false; } if(index >= 1) cout << setw(11) << setfill(' ') << left << 1; // 固定长度为11,左对齐 if(index >= 2) cout << setw(11) << setfill(' ') << left << 1; int n1 = 1, n2 = 1, elem; for(int i = 3; i <= index; i++) { elem = n1 + n2; n1 = n2; n2 = elem; cout << setw(11) << setfill(' ') << left << elem << (i % 5 == 0 ? "\n" : ""); // 固定长度为11,左对齐,每行打印5个数 } cout << endl; return true; }
2.2 调用函数
-
交换两个变量的值的函数
- 按值传递pass by value:函数无法达到交换的目的
void swap(int val1, int val2) { // 错误写法,不能改变原val1和val2的值 int tmp = val1; val1 = val2; val2 = tmp; } int main() { int a = 0, b = 1; swap(a, b); cout << a << " " << b << endl; // 输出"0 1",并未交换a和b的值 return 0; }
(1) 传给swap()的对象被复制了一份,原对象和副本之间没有任何关联,即swap函数中的val1, val2分别和main函数中的a, b,仅仅是存储相同的值,并非同一个对象
(2) 当我们调用函数时,会在内存中建立程序堆栈(program stack),提供每个函数参数以及函数定义的每个对象(统称为局部对象)的存储空间。函数完成后,这些内存就被释放掉,或者说从程序堆栈中被pop出来
- 传址pass by reference
void swap(int &val1, int &val2) { // 参数声明为引用类型 int tmp = val1; val1 = val2; val2 = tmp; } int main() { int a = 0, b = 1; swap(a, b); // 调用函数语句无需改变 cout << a << " " << b << endl; // 输出"1 0",交换a和b的值 return 0; }
-
对一个vecotr内的整数值排序(冒泡排序)的函数
其中swap函数和bubble_sort函数均是用了pass by reference的方式传递参数
#include <iostream> #include <vector> using namespace std; void display(vector<int> &vec); void swap(int &val1, int &val2); // 冒泡排序子程序传递分别绑定俩变量的引用给交换子程序 void bubble_sort(vector<int> &vec); // 主函数传递绑定vec的引用给冒泡排序子程序 int main() { int a[8] = {8, 34, 3, 13, 1, 21, 5, 2}; vector<int> vec(a, a+8); cout << "vector before sort: "; display(vec); cout << "vector after sort: "; bubble_sort(vec); display(vec); return 0; } void bubble_sort(vector<int> &vec) { // 冒泡排序, 时间复杂度O(n^2) for(int i = 0; i < vec.size(); i++) // 每次迭代vec[i]到vec[vec.size()-1]中最小的元素冒泡至vec[i] for(int j = i + 1; j < vec.size(); j++) if(vec[j] < vec[i]) swap(vec[i], vec[j]); } void swap(int &val1, int &val2) { // pass by reference, 参数必须为引用类型,若为int类型,则不能改变原对象的值 int tmp = val1; val1 = val2; val2 = tmp; } void display(vector<int> &vec) { for(int i = 0; i < vec.size(); i++) cout << vec[i] << ' '; cout << endl; }
程序执行结果
vector before sort: 8 34 3 13 1 21 5 2 vector after sort: 1 2 3 5 8 13 21 34
-
pass by reference
(1) 面对引用的所有操作,都相当于面对"引用绑定的对象"的操作
int i = 1024, &r = i, *p = &i; int j = 4096; r = j; // 将j的值赋给r绑定的对象,即i的值变为4096 p = &r; // 将r绑定对象的地址赋给p,即p指向r绑定对象,而非让p指向r // 事实上,引用并非对象,故指针无法指向引用
(2) 例外:decltype函数参数为引用时,将返回引用的类型,而非引用所指对象的类型
int a = 0, &b = a; decltype(b) c = a; // b为int &型,decltype返回该类型,故c为int &型,绑定变量a
(3) pass by reference的好处
1) 使函数能够直接对所传入的对象进行修改
2) 降低复制大型对象的额外负担
如bubble_sort函数可以声明如下,传入vec对象的拷贝,并把排序后的vector对象返回给主函数,也能达到同样的目的。但相对于第一种声明方式,复制vector对象使用了额外的存储空间以及复制vector对象的时间
// void bubble_sort(vector<int> &vec); vector<int> bubble_sort(vector<int> vec); // 占用额外的存储空间
又如在display函数中,可以声明如下,传入vec对象的拷贝,但缺点如上所述
// void display(vector<int> &vec); void display(vector<int> vec);
(4) const + pass by reference
进一步地,若使用pass by reference时,函数不会改变所传递对象,则可以声明为底层const
因为我们不会在display中改变vector对象,所以可以声明如下,该常量引用参数将指向main函数中的非常量对象(这不会引起问题,但不能让非常量引用绑定常量对象)
void display(const vector<int> &vec) { for(int i = 0; i < vec.size(); i++) cout << vec[i] << ' '; cout << endl; }
-
以指针方式传递实现传址
#include <iostream> #include <vector> using namespace std; void display(const vector<int> *); void swap(int *, int *); void bubble_sort(vector<int> *); int main() { int a[8] = {8, 34, 3, 13, 1, 21, 5, 2}; vector<int> vec(a, a+8); cout << "vector before sort: "; display(&vec); cout << "vector after sort: "; bubble_sort(&vec); display(&vec); return 0; } void bubble_sort(vector<int> *vec) { for(int i = 0; i < (*vec).size(); i++) // 每次迭代vec[i]到vec[vec.size()-1]中最小的元素冒泡至vec[i] for(int j = i + 1; j < (*vec).size(); j++) if((*vec)[j] < (*vec)[i]) swap(&(*vec)[i], &(*vec)[j]); // 将vec指针所指vector对象的第i和j个元素的地址传递给swap函数 } void swap(int *val1, int *val2) { int tmp = *val1; *val1 = *val2; *val2 = tmp; } void display(const vector<int> *vec) { for(int i = 0; i < (*vec).size(); i++) { cout << (*vec)[i] << ' '; } cout << endl; }
-
作用域及范围
(1) 除了static例外,函数内定义的对象只存在于函数执行期间,函数执行完后会从程序堆栈中pop()以释放内存空间
(2) 不应返回函数内定义的局部对象的地址(引用或指针),否则程序可能会对不存在的对象进行寻址操作,正确的方式是按值传递,返回局部对象的副本,它在函数之外依然存在
(3) 作用域
1) 对象在程序内的存活区域称为对象的作用域scope
2) 在函数内定义的局部对象具有local scope,在函数外声明的对象具有file scope
- 内置类型的对象,若在file scope内定义则被默认初始化为0(指针默认初始化为空指针,bool类型默认初始化为false,引用不是对象),若在local scope内定义则不会进行默认初始化
-
动态内存管理
(1) local scope和file scope均由系统自动管理,另一种存储期形式为动态范围dynamic extent,其内存由程序的空闲空间分配而来,也成为堆内存heap memory,其内存由程序员自行管理,通过new表达式分配,通过delete表达式释放
(2) new表达式
int *pi = new int(100); // new Type int *pi = new int(100); // new Type(initial_value)
new Type由heap分配一个类型为Type的对象,返回该对象的地址,默认情况下heap分配的对象皆未初始化
int *p = new int; // heap分配的对象不初始化,即使在file scope也一样 int a, *pa = &a; // a未初始化,但在file scope中定义,故将默认初始化为0 int main() { cout << *p << endl; // p所指int对象未初始化,将输出奇怪的值 cout << *pa << endl; // pa所指int对象默认初始化为0,将输出0 }
程序输出结果
38673552 0
new Type(initial_value)由heap分配一个类型为Type的对象,初始化为initial_value,并返回其地址
(3) delete表达式
heap分配的对象具有动态范围,可以持续存活,知道delete表达式将其释放
int *pi = new int(1024); delete pi;
(4) 数组操作
int *pia = new int[24]; // 从heap中分配一个数组,拥有24个整数,均未初始化,返回第一个数组元素地址 delete [] pia; // 删除数组中的所有对象,在delete和数组指针之间加上[]
(5) 内存泄漏memory leak
若程序员不适用delete表达式释放heap分配的对象,则其永远不会被释放,应该防止memory leak的发生
2.3 提供默认参数值
-
一般地,以参数传递作为函数间的沟通方式,优于直接将对象定义于file scope,否则我们必须了解函数和file scope中对象的工作逻辑,程序也可能难以在其他环境重用
-
在冒泡程序中将跟踪信息打印到ofil文件
void bubble_sort(vector<int> &vec, ofstream *ofil = nullptr); void bubble_sort(vector<int> &vec, ofstream *ofil) { // 冒泡排序, 时间复杂度O(n^2) for(int i = 0; i < vec.size(); i++) // 每次迭代vec[i]到vec[vec.size()-1]中最小的元素冒泡至vec[i] for(int j = i + 1; j < vec.size(); j++) if(vec[j] < vec[i]){ if(ofil) // 若ofil不为nullptr,则将交换信息输出至ofil对象 (*ofil) << "about to call swap! i: " << i << " j: " << j << "\tswapping: " << vec[i] << " with " << vec[j] << endl; swap(vec[i], vec[j], ofil); } } int main(){ // ... bubble_sort(vec); // ofil默认为nullptr,程序不打印交换信息 ofstream ofil("data.txt"); bubble_sort(vec, &ofil); // 传入ofil,程序将交换信息输出至ofil对象,即data.txt }
-
默认参数值规则
(1) 默认值的解析是从最右边进行的,若为某个参数提供了默认值,则这一参数右侧的所有参数也必须具有默认参数值
void fun1(int a, int b = 0, int c); // 非法,b及其右侧的所有参数都得具有默认参数值 void fun2(int a, int b = 0, int c = 1); // 合法
(2) 默认值只能指定一次,可以在函数声明处或函数定义处,但不能两个地方都指定。可把默认值指定放在函数声明处,函数声明放在头文件中,程序通过包含对应头文件的方式调用该函数,从而提高函数的可见性(visiblity)
2.4 使用局部静态对象
-
局部对象在函数执行完成后会被释放,而为了节省函数间通信问题而在file scope中定义对象不是一种合适的做法
-
静态局部对象local static object
(1) 局部静态对象在函数内定义,用关键字static标识,该函数执行完成后不会立刻释放该对象的内存空间,并在不同的函数调用过程中持续存在
const vector<int> * fibon_seq(int size){ static vector<int> elems; // ... return &elems; }
(2) 打印Fibonacci数列前n个数,使用局部静态对象作为存储Fibonacci序列的vector对象
#include <iostream> #include <vector> using namespace std; vector<int>* fibonnaci_seq(int size); void display(vector<int>*, int); int main(){ int size; while(cin >> size) display(fibonnaci_seq(size-1), size); return 0; } void display(vector<int>* p, int size){ // 输出Fibonacci数列的前size个元素 if(p){ for(int i = 0; i < size; i++) cout << (*p)[i] << ' '; cout << endl; } } vector<int>* fibonnaci_seq(int size){ const int max_size = 45; if(size < 0 || size >= max_size){ cerr << "invalid size" << endl; return nullptr; } static vector<int> elem; // 定义为空的vector,利用push_back()将数值放在vector末端 for(int i = elem.size(); i <= size; i++){ // 若elem已保存相应元素,则无需重复计算 if(i <= 1) elem.push_back(1); // push_back()将数值放在vector末端 else elem.push_back(elem[i-1] + elem[i-2]); } return &elem; }
2.5 声明inline函数
-
重构fibon_elem()函数,将各个小功能分解为独立的函数
inline bool is_size_ok(int size) { // 函数声明为inline const int max_size = 45; if(size < 0 || size > max_size){ cerr << "invalid size" << endl; return false; } return true; } int fibo_seq(int size) { if(!is_size_ok(size)) return 0; // ... }
-
函数声明为inline,表示要求编译器在每个函数调用点上,将函数的内容展开,通过减少函数调用可能获得性能改善
-
函数指定为inline,只是对编译器提出了要求,而编译器是否执行该请求,视编译器而定
-
一般将体积小、常被调用且并不复杂的函数声明为inline
-
编译器必须在inline函数被调用时将其展开,故必须保证此时inline函数的定义是有效的,因此inline函数一般定义在头文件中
2.6 提供重载函数
-
对于同一个函数(名),传入不同类型甚至不同数量的参数,利用了函数重载机制function overloading
-
参数列表不相同(类型或个数不相同均可)的多个函数,可以拥有相同的函数名称。编译器根据调用时传入的实际参数,决定调用哪一个函数
-
编译器能根据参数列表区分函数名相同的两个函数,但无法通过返回类型区分
ostream& display_message(char ch); bool display_message(char ch); // 错误,不能靠返回类型区分同名函数 display_message('a'); // 编译器无法确定调用哪个函数
-
display_message函数
#include <iostream> using namespace std; void display_message(); void display_message(int); void display_message(string); void display_message(int, string); int main(){ int a = 1; string s = "Hello world"; display_message(); display_message(a); display_message(s); display_message(a, s); return 0; } void display_message(){ cout << "an example of function overloadling" << endl; } void display_message(int msg){ cout << msg << endl; } void display_message(string msg){ cout << msg << endl; } void display_message(int msg1, string msg2){ cout << msg1 << " " << msg2 << endl; }
2.7 定义并使用模板函数
- 如下所示,多个display_message()函数处理不同的参数,但每个函数体都颇为相似,我们可以使用函数模板function template机制
void display_message(const string&, const vector<int>&);
void display_message(const string&, const vector<double>&);
void display_message(const string&, const vector<string>&);
void display_message(const string &msg, const vector<int> &vec){ // 三者的函数体一致
cout << msg;
for(int i = 0; i < vec.size(); i++)
cout << vec[i] << ' ';
}
// ...
-
function template将参数列表中指定的部分或全部参数的类型信息抽取出来
-
function template以template开头,紧接着以
<>
包围一或多个标识符,用以表示我们希望推迟决定的数据类型,用于利用这一模板产生函数时必须提供类型信息template <typename elemType> // typename关键字表示elemType是一个暂缓决定类型的占位符 void display_message(const string &msg, const vector<elemType> &vec){ cout << msg; for(int i = 0; i < vec.size(); ++i) { elemType t = vec[i]; cout << t << ' '; } } int main(){ string arr[] = {"Hello", "world"}; vector<string> vec(arr, arr + 2); display_message("", vec); // 直接调用,编译器会将elemType绑定为string类型 return 0; }
typename关键字表示elemType在display_message()函数中是一个暂时放置类型的占位符,elemType是个任意的名称
-
function template的参数通常由两种类型构成,一是明确的类型(如上例中的msg),另一类是暂缓决定的类型(上例中的vec)
-
模板函数中由多个暂缓决定的类型
template <typename msgType, typename elemType> // msgType和elemType为暂缓决定的类型 void display_message(const msgType &msg, const vector<elemType> &vec){ cout << msg << endl; for(int i = 0; i < vec.size(); ++i){ elemType t = vec[i]; cout << t << ' '; } }
-
重载与模板函数的选择
(1) 一般来说,若函数具有多种实现方式,可以将其重载
(2) 若希望程序代码的主体不变,只改变其中用到的数据类型,则通过function template达到目的
-
function template同时也是重载函数
template <typename elemType> // 第一个display_message函数的第二个参数为vector类型 void display_message(const string &msg, const vector<elemType> &vec); template <typename elemType> // 第二个display_message函数的第二个参数为list类型 void display_message(const string &msg, const list<elemType> &li)
2.8 函数指针
吐槽:本节标题为Pointers to Functions Add Flexibility,可加上also much complexity
-
通过vector返回六种数列,需要六个函数
const vector<int> *fibon_seq(int size); const vector<int> *lucas_seq(int size); const vector<int> *pell_seq(int size); const vector<int> *triang_seq(int size); const vector<int> *square_seq(int size); const vector<int> *pent_seq(int size); const vector<int> *fibon_seq(int size) { const int max_size = 45; static vector<int> elems; if(size <= 0 || size > max_size) { cerr << "invalid size" << endl; return 0; } for(int i = elems.size(); i < size; i++) { if(i <= 1) elems.push_back(1); else elems.push_back(elems[i-1] + elems[i-2]); } return &elems; } // 其余五个数列对应的函数 // 判断能否返回Fibonacci序列对应索引的元素,还需另外定义lucas_elem等五个函数? bool fibo_elem(int pos, int &elem) { const vector<int> *pseq = fibo_seq(pos); // 唯一和数列相关的部分,调用fibo_seq函数 if(!pseq){ elem = 0; return false; } elem = (*pseq)[pos-1]; return true; } // 能否消除与fibo_seq的关联,从而用一个函数seq_elem实现对所有六个数列的判断?
-
函数指针
函数指针pointer to function必须指明所指函数的返回类型和参数列表
// 指向一类函数的指针,函数必须以一个int为参数,返回类型必须为指向const vector<int>的指针 const vector<int>* (*seq_ptr) (int); // seq_ptr可指向上述的fibon_seq等六个函数
定义函数指针后,编写seq_elem使其能服务于六个数列
bool seq_elem(int pos, int &elem, const vector<int>* (*seq_ptr) (int)) { const vector<int> *pseq = seq_ptr(pos); // 调用seq_ptr指向的函数 if(!pseq){ elem = 0; return false; } elem = (*pseq)[pos-1]; return true; }
-
函数的地址:提供函数的名称即可
seq_ptr = fibo_seq; // 将fibo_seq()函数的地址赋给函数指针seq_ptr
为了使seq_ptr方便地指向不同的数列函数,可以定义一个存放函数地址的数组
const vector<int>* (*seq_array[])(int) = { fibo_seq, lucas_seq, pell_seq, triang_seq, square_seq, pent_seq };
借助枚举类型存放的常量对上述数组进行索引操作
enum ns_type{ ns_fibo, ns_lucas, ns_pell, ns_triang, ns_square, ns_pent // 各项称为枚举项,其值依次为0, 1, 2, 3, 4, 5 };
调用时,可使用对应的枚举项作为数组索引值
seq_ptr = seq_array[ns_fibo];
2.9 设定头文件
-
为了避免在多个文件中进行重复的函数声明,可以把函数声明放在头文件中,并在每个程序代码文件内包含这些函数声明,这也减少了维护函数声明的工作量
-
把与数列处理有关的函数的声明都放在一个头文件内,扩展名一般为
.h
// NumSeq.h bool seq_num(int pos, int &elem); const vector<int> *fibon_seq(int size); const vector<int> *lucas_seq(int size); const vector<int> *pell_seq(int size); const vector<int> *triang_seq(int size); const vector<int> *square_seq(int size); const vector<int> *pent_seq(int size);
-
把函数声明放在头文件,但一般不把函数定义放在头文件,因为函数定义只有一份,若多个程序文件引用同一个头文件则可能造成重复定义
例外:inline函数应该放在头文件内,因为在每个inline函数的调用点上,编译器必须取得函数定义以将其展开
-
file scope定义的对象,若可能被多个文件访问,也应该被声明于头文件中,声明时需要使用extern关键字
const int seq_cnt = 6; // const对象不需要加extern,因为const object无法跨文件访问,重新定义即可 extern const vector<int>* (*seq_array[seq_cnt]) (int); // 声明seq_array对象
-
尖括号与双引号的区别
#include <iostream> #include "NumSeq.h"
若头文件被认为是标准的或项目专属的头文件,用尖括号括住,编译器将从某些默认的磁盘目录中开始寻找文件;若是用户提供的头文件,则用双引号括住,编译器将从包含此文件的程序所在的磁盘目录中开