機能と演習ソリューション
1. 基本
関数には、戻り値の型、関数名、0 個以上の *パラメーター* のリスト、および関数本体が含まれます。
関数はcall 演算子を使用して呼び出すことができます。呼び出し演算子の形式の 1 つは括弧のペア () で、関数または関数へのポインタである式に作用します。括弧内には実)リスト。関数パラメータを初期化するために使用されます。呼び出し式の型は関数の戻り値の型です。
関数の作成と呼び出し
//定义函数:fact 用来阶乘
// val的阶乘是val * (val - 1) * (val - 2) . . . * ((val - (val - 1)) * 1)
int fact(int val)
{
int ret = 1; // 局部变量,用于保存计算结果
while (val > 1)
ret *= val--; // 把ret和val的来积赋给ret,然后将val减1
return ret; // 返回结果
}
//调用函数
int main()
{
int j = fact(5); // j equals 120, i.e., the result of fact(5)
cout << "5! is " << j << endl;
return 0;
}
関数呼び出しは 2 つのタスクを完了します。1 つは対応する仮パラメータを実際のパラメータで初期化することで、もう 1 つは呼び出し元の関数から呼び出された関数に制御を移すことです。
このとき、呼び出し元の関数(calling function)の実行は一時的に中断され、呼び出された関数(called function)の実行が開始されます。
return ステートメントに遭遇すると、関数は実行プロセスを終了し、2 つのタスクを完了します: 1 つは return ステートメントの値 (存在する場合) を返すこと、もう 1 つは呼び出された関数から呼び出した関数に制御権を戻すことです。 。
仮パラメータと実パラメータ
実パラメータは仮パラメータの初期値であり、2 つの順序と数量は一致する必要があります。
前のコード部分のファクト関数の場合:
fact("hello");
fact();
fact(23, 34);
fact(3.4);
float は int に変換できるため、4 番目のみ正常に呼び出すことができます。1 つ目は型を変換できないこと、2 つ目と 3 つ目は実パラメータが間違っていることです。
関数パラメータリスト
関数の仮パラメータリストは空にすることもできますが、省略することはできません。仮パラメータ リスト内の仮パラメータは、通常、カンマで区切られます。各仮パラメータは、宣言子を含む宣言です。2 つの仮パラメータの型が同じであっても、両方の型宣言を書き出す必要があります。
void f1() {
/* ... */ } // 隐式地定义空形参列表
void f2(void) {
/* ... */ } // 显式地定义空形参列表
int f3(int v1, v2) {
/* ... */ } // 错误
int f4(int v1, int v2) {
/* ... */ } // 正确
関数の 2 つのパラメーターに同じ名前を付けることはできません。また、関数の最も外側のスコープにあるローカル変数は、関数パラメーターと同じ名前を使用することはできません。
仮パラメータの名前はオプションですが、名前のない仮パラメータは使用できないため、通常、仮パラメータには名前が必要です。仮パラメータが関数で使用されない場合でも、実パラメータを指定する必要があります。
関数の戻り値の型は配列型または関数型にすることはできませんが、配列または関数へのポインターにすることはできます。
演習 6.1: 実際のパラメータと形式的なパラメータの違いは何ですか?
回答: 仮パラメータは関数が定義されている場所に表示され、仮パラメータ リストには 0、1 つ以上の仮パラメータを含めることができ、複数の仮パラメータはカンマで区切られます。仮パラメータは、関数が受け入れるデータのタイプと量を指定します。
実パラメータは、関数が呼び出される場所に表示されます。実パラメータの主な機能は、仮パラメータを初期化することです。実パラメータの数は、仮パラメータと同じである必要があります。実パラメータの型は、対応する仮パラメータと一致する必要がありますパラメータの型、または実パラメータの型を仮パラメータの型に変換できます。
演習 6.2: 次の関数のどれが間違っているか、またその理由を教えてください。これらのエラーはどのように修正すべきでしょうか?
(a) int f() {
string s;
// ...
return s;
}
(b) f2(int i) {
/* ... */ }
(c) int calc(int v1, int v1) /* ... */ }
(d) double square (double x) return x * x;
回答:
(a) は誤りです。関数本体によって返される結果の型は string ですが、関数の戻り値の型は int であるため、この 2 つは矛盾しており、自動的に変換できません。
string f() {
string s;
// ...
return s;
}
(b) は関数に戻り値の型がないため false です。関数が値を返す必要がない場合は、プログラムを次のように変更する必要があります。
void f2(int i) {
/* ... */ }
(c) は誤りで、同じ関数に複数の仮パラメータが含まれる場合、これらの仮パラメータの名前を繰り返すことはできず、また、関数本体の左側の中括弧が欠落しています。
変更されたプログラムは次のようになります。
double square (double x) {
return x * x; }
(d) は誤りです。関数本体は一対の中括弧で囲む必要があるためです。
double square (double x) {
return x * x; }
演習 6.3: 独自のファクト関数を作成し、それがコンピューター上で正しいかどうかを確認します。
答え:
int fact(int i)
{
if (i < 0)
return -1;
int sum = 0;
sum = i > 1 ? i * fact(i - 1) : 1;
return sum;
}
演習 6.4: ユーザーと対話し、ユーザーに数値の入力を求め、その数値を生成する階乗を計算する関数を作成します。この関数は main 関数内で呼び出します。
答え:
#include <iostream>
using namespace std;
int fact(int i)
{
if (i < 0)
return -1;
int sum = 0;
sum = i > 1 ? i * fact(i - 1) : 1;
return sum;
}
int main()
{
int val, sum = 0;
cin >> val;
cout << val << " 的阶乘是 " << fact(val) << endl;
return 0;
}
演習 6.5: 引数の絶対値を出力する関数を作成します。
答え:
#include <iostream>
#include <cmath>
using namespace std;
double myABS(double val)
{
if (val < 0)
return val * -1;
else
return val;
}
double sysABS(double val)
{
return abs(val);
}
int main()
{
double num;
cout << "请输入一个数:";
cin >> num;
cout << num << " 的 myABS 绝对值是 " << myABS(num) << endl;
cout << num << " 的 sysABS 绝对值是 " << sysABS(num) << endl;
return 0;
}
1.1 ローカルオブジェクト
オブジェクトの存続は、プログラムの実行中にオブジェクトが存在する期間です。
関数本体で定義された仮パラメータと変数は関数のスコープ内でのみ表示され、ローカル変数は外側のスコープ内の同じ名前を持つ他のすべての宣言で隠されます。
関数の本体で定義されたオブジェクトは、プログラムの実行中に存在し、プログラムの開始時に作成され、プログラムの終了時に破棄されます。
自動オブジェクト
通常のローカル変数に対応するオブジェクトの場合、関数は定義されている場所にオブジェクトを作成し、ブロックの最後でそれを破棄します。ブロックの実行中に存在するオブジェクトは、自動。
仮パラメータとローカル変数は両方とも自動オブジェクトです。仮パラメータに対応する自動オブジェクトは実パラメータの値で初期化されます。ローカル変数に対応する自動オブジェクトに初期値が含まれている場合は、その初期値で初期化されます。それが含まれていない場合、それは未定義です。
ローカル静的オブジェクト
ローカル静的オブジェクト (ローカル静的オブジェクト) は、ローカル変数を静的型として定義し、その宣言は関数呼び出しとその後の全体にわたって収集されます。
演習 6.6: 仮パラメータ、ローカル変数、およびローカル静的変数の違いを説明します。3 つの形式をすべて使用する関数を作成します。
回答:関数本体内で定義された仮パラメータと変数は、総称してローカル変数と呼ばれます。これらは関数に対してローカルです。関数の制御パスが変数定義ステートメントを通過すると、オブジェクトが作成され、次の時点で破棄されます。定義が存在するブロックの最後に到達するため、関数のスコープ内でのみ表示され、関数本体内のローカル変数は通常のローカル変数と静的ローカル変数に分けられます。ブロック実行中にのみ存在するオブジェクト(通常の静的変数)を自動オブジェクトと呼びます。
ローカル静的変数は特別であり、そのライフサイクルは関数呼び出し以降も実行されます。ローカル静的変数に対応するオブジェクトはローカル静的オブジェクトと呼ばれ、そのライフサイクルは定義文から始まりプログラムが終了するまで終了しません。
#include <iostream>
using namespace std;
double myAdd(double val1, double val2){
// val1和val2是形参
double result = val1 + val2; // result是普通局部变量
static unsigned iCnt = 0; // iCnt是静态局部变量
++iCnt;
cout << "该函数已经累计执行了" << iCnt << "次" << endl;
return result;
}
int main(){
double num1, num2;
cout << "请输入两个数:";
while (cin >> num1 >> num2)
{
cout << num1 << "与" << num2 << "的求和结果是:"
<< myAdd(num1, num2) << endl;
}
return 0;
}
演習 6.7: 初めて呼び出されたときに 0 を返し、呼び出されるたびに 1 ずつ増加する関数を作成します。
答え:
#include <iostream>
using namespace std;
unsigned myCnt(){
static unsigned iCnt = -1;
++iCnt;
return iCnt;
}
int main(){
cout << "请输入任意字符后按回车键继续" << endl;
char ch;
while (cin >> ch)
{
cout << "函数myCnt()执行的次数是:" << myCnt() << endl;
}
return 0;
}
1.2 関数宣言
変数と同様に、関数は使用前に宣言する必要があり、定義できるのは 1 回だけですが、複数回宣言することもできます。関数を使用しない場合は、定義せずに宣言するだけで済みます。
関数の宣言は関数の定義と似ており、関数本体は含まれず、仮パラメータも含まれない場合があります。
関数の戻り値の型、関数名、および仮パラメータの型は、関数のインターフェイスを表します。関数宣言は、関数プロトタイプとも呼ばれます。
ヘッダファイル内の関数宣言
関数はヘッダー ファイルで宣言し、元のファイルで定義することをお勧めします。
ヘッダー ファイルで関数を宣言すると、同じ関数のすべての宣言の一貫性が保証されます。関数インターフェイスを変更する場合は、1 つの宣言を変更するだけで済みます。
演習 6.8: セクション 6.1 の演習で得た関数宣言を含む Chapter6.h というヘッダー ファイルを作成します。
答え:
#ifndef CHAPTER6_H_INCLUDED
#define CHAPTER6_H_INCLUDED
int fact(int);
double myABS(double);
double sysABS(double);
#endif // CHAPTER6_H_INCLUDED
1.3 個別のコンパイル
個別コンパイル(個別コンパイル)では、プログラムを論理的関係に従って複数のファイルに分割し、各ファイルを独立してコンパイルします。このプロセスにより、通常、オブジェクト コードを含む拡張子 .obj (Windows) または .o (UNIX) を持つファイルが生成されます。次に、コンパイラはオブジェクト ファイルをリンクして実行可能ファイルを形成します。
例については演習を参照してください。
演習 6.9: 独自の fat.cc と fatMain.cc を作成します。どちらのファイルにも、前のセクションの演習で作成した Chapter6.h ヘッダー ファイルが含まれている必要があります。これらのファイルを使用して、コンパイラが個別のコンパイルをサポートする方法を理解します。
答え:
事実.cpp:
#include "Chapter6.h"
using namespace std;
int fact(int val)
{
if (val < 0)
return -1;
int ret = 1;
for (int i = 1; i != val; ++i){
ret *= i;
}
return ret;
}
ファクトメイン.cpp:
#include <iostream>
#include "Chapter6.h"
using namespace std;
int main()
{
int num;
cout << "请输入一个数:";
cin >> num;
cout << num << " 的阶乘是:" << fact(num) << endl;
return 0;
}
最初の 2 つのコマンドは、まず 2 つのファイルを個別にコンパイルし、3 番目のコマンドは 2 つの .o ファイルを実行可能プログラムにリンクします。
両方のファイルを直接コンパイルすることも可能です。
cpp ファイルをコンパイルして直接リンクすることもできます。
2. パラメータの受け渡し
関数が呼び出されるたびに、その仮パラメータが再作成され、渡された引数で初期化されます。仮パラメータの初期化のメカニズムは、変数の初期化のメカニズムと同じです。
仮パラメータが参照型の場合、対応する実パラメータが参照、または関数が参照によって呼び出されると言います。参照パラメータは、バインド先のオブジェクトのエイリアスです。
仮パラメータの値を仮パラメータにコピーすると、仮パラメータと実パラメータは独立した2つのオブジェクトになります。このような引数は値によって渡される、または関数は値によって呼び出されると言います。
2.1 値渡しパラメータ
非参照型パラメータを初期化するには、初期値がパラメータにコピーされ、パラメータを変更しても実際のパラメータには影響しません。
ポインタパラメータ
同じことがポインタ パラメータにも当てはまり、パラメータ ポインタの値は実際のパラメータと同じですが、この 2 つはオブジェクトではありません。それが参照するオブジェクトは、仮パラメータ ポインタを通じて操作できます。
// 该函数接受一个指针,然后将指针所指的位置为0
void reset(int *ip) {
*ip = 0; // 改变指针ip所指对象的值
ip = 0; // 只改变了ip的局部拷贝,实参未被改变
}
リセット関数を呼び出した後、実パラメータが指すオブジェクトは 0 に設定されますが、実パラメータ自体は変更されません。
int i = 42;
reset(&i); // 改变i的值而非i的地址
cout << "i = " << i << endl; // 输出i = 0
演習 6.10: ポインター パラメーターを使用して 2 つの整数の値を交換する関数を作成します。コード内でこの関数を呼び出し、交換された結果を出力して、関数が正しいことを確認します。
答え:
#include <iostream>
using namespace std;
// 在函数体内部通过解引用操作改变指针所指的内容
void mySwap(int *p, int *q){
int tmp = *p;
*p = *q;
*q = tmp;
}
int main(){
int a = 5, b = 10;
int *r = &a, *s = &b;
cout << "交换前:a = " << a << ",b = " << b << endl;
// 指针形参
mySwap(r, s);
// 引用形参(建议)
// mySwap(&a, &b);
cout << "交换后:a = " << a << ",b = " << b << endl;
return 0;
}
// 在函数体内部交换了两个形参指针本身的值,未能影响实参
void mySwap(int *p, int *q){
int *tmp = p;
p = q;
q = tmp;
}
2.2 参照パラメータの受け渡し
参照パラメータを使用すると、関数は実際のパラメータの値を変更できます。
// 该函数接受一个int对象的引用,然后将对象的位置为0
void reset(int &i) // i是传给reset函数的对象的另一个名字
{
i = 0; // 改变了i所引对象的值
}
参照パラメータは、初期化されたオブジェクトをバインドします。Reset が呼び出されると、i は関数に渡された int オブジェクトにバインドされ、この時点で i を変更すると、i によって参照されるオブジェクトの値も変更されます。
int j = 42;
reset(j); // j采用传引用方式,它的值被改变
cout << "j = " << j << endl; // 输出j = 0
i は j の別名であり、関数内で i に 0 を代入することは、実パラメータ j に 0 を代入することを意味します。
コピーを避けるために参照を使用する
大きなクラス タイプのオブジェクトやコンテナ オブジェクトをコピーするのは非効率的です。また、一部のクラス タイプ (IO タイプなど) はコピー操作をまったくサポートしません。現時点では、このタイプのオブジェクトには参照パラメータを介してのみアクセスできます。
関数が参照パラメータの値を変更する必要がない場合は、それを const 参照として宣言することをお勧めします。
参照パラメータを使用して追加情報を返す
関数は 1 つの値のみを返すことができますが、関数が同時に複数の値を返す必要がある場合は、reference パラメーターを使用して関数が追加情報を返すようにすることができます。
演習 6.11 : 参照型のパラメーターで動作する独自のリセット関数を作成して検証します。
答え:
#include <iostream>
using namespace std;
void reset(int &i){
i = 0;
}
int main(){
int num = 5;
cout << "重置前:num = " << num << endl;
reset(num);
cout << "重置前:num = " << num << endl;
return 0;
}
演習 6.12 : セクション 6.2.1 の演習のプログラムを書き直して、2 つの整数の値をポインタではなく参照によって交換します。どの方法が使いやすいと思いますか? なぜ?
答え:
#include <iostream>
using namespace std;
void mySwap(int &p, int &q){
int tmp = p;
p = q;
q = tmp;
}
int main(){
int a = 5, b = 10;
int *r = &a, *s = &b;
cout << "交换前:a = " << a << ",b = " << b << endl;
mySwap(a, b);
cout << "交换后:a = " << a << ",b = " << b << endl;
return 0;
}
ポインターを使用する場合と比較して、参照を使用して変数の内容を交換する形式は単純であり、追加のポインター変数を宣言する必要がなく、ポインターの値のコピーも回避されます。
C++ では、ポインター パラメーターの代わりに参照パラメーターを使用することをお勧めします。
演習 6:13 : T が何らかの型の名前であると仮定して、次の 2 つの関数宣言の違いを説明してください。1
つは void f(T) で、もう 1 つは void f(&T) です。
回答:
最初の関数は値によって呼び出され、T の変更は実際のパラメータに影響しません。2 番目の関数は参照によって呼び出され、T の変更は実際のパラメータに影響します。
演習 6.14 : 仮パラメータを参照型にする必要がある例と、仮パラメータを参照型にできない別の例を挙げてください。
回答: 関数が複数の値を返す必要がある場合、大容量のコンテナーや大きなオブジェクトを操作する必要がある場合は参照を使用します。
整数の階乗を計算する場合は参照を渡す必要はありません。
演習 6.15 : find_char 関数の 3 つの仮パラメータが現在の型である理由を説明してください。特に s が定数参照であり、occurs が共通参照である理由を説明してください。
s と happens は参照型であるのに、 c は参照型ではないのはなぜですか?
を普通の参照にするとどうなるでしょうか?
発生を定数参照にするとどうなるでしょうか?
回答: find_char 関数の 3 つのパラメーターの型設定は、関数の処理ロジックと密接に関連しており、その理由は次のとおりです。
検索対象の文字列 s は、長い文字列のコピーを避けるために参照型を使用すると同時に、文字列の内容は変更せずに検索のみを行うため、定数参照として宣言します。
検索対象の文字 c の型は char で、占有するのは 1 バイトだけであり、コピーのコストは非常に低く、実際のパラメータのメモリに格納されている実際の内容を操作する必要はなく、コピーするだけです。その値を仮パラメータに設定するだけなので、参照型を使用する必要はありません。
文字の出現数については、関数内の実パラメータ値の変更を関数外に反映する必要があるため、参照型として定義する必要がありますが、定数参照として定義できない場合は定義できません。内容が変更となります。
2.3 const仮パラメータと実パラメータ
const の詳細については、入門書の第 1 章を参照してください。
仮パラメータにトップレベルの const がある場合、それを定数オブジェクトまたは非定数オブジェクトに渡すことができます。
void fun(const int i){
}//调用时可以传入 const int 或者 int
ポインタまたは参照パラメータ const
低レベルの const 参照はバインドできるか、低レベルの const が非定数を指すことができるため、非 const オブジェクトを使用して低レベルの const パラメータを初期化できます (参照とポインタのみが低レベルの const を持ちます)。 const オブジェクト本体。しかし、その逆はありません。
可能な限り定数参照を使用する
関数の不変の仮パラメータを通常の参照として定義すると、関数が受け入れることができる実パラメータのタイプが大幅に制限され、また、関数が実パラメータの値を変更できるという誤解を招くことになります。また、const 参照ではなく参照を使用すると、関数が受け入れることができる引数の種類が大幅に制限される可能性があります。
演習 6.16 : 次の関数は正当ですが、特に有用ではありません。その限界を指摘し、それを改善する方法を見つけてください。
bool is_empty(string& s) {
return s.empty(); }
回答:
このプログラムはパラメータ タイプを非定数参照として設定します。これにはいくつかの欠陥があります。
1 つは、ユーザーを誤解させやすいこと、つまり、プログラムが s の内容の変更を許可していること、もう 1 つは、関数が
受け取ることができる実際のパラメータの型が制限されており、const オブジェクト、リテラル定数、または、通常の参照パラメータへの型変換が必要なオブジェクト。
bool is_empty(const string& s) {
return s.empty(); }
演習 6.17 : 文字列オブジェクトに大文字が含まれているかどうかを判断する関数を作成します。
すべての文字列オブジェクトを小文字に書き換える別の関数を作成します。
両方の関数で同じパラメータ型を使用していますか? なぜ?
答え:
#include <iostream>
#include <string>
using namespace std;
bool HasUpper(const string& str){
for (auto c : str)
if (isupper(c))
return true;
return false;
}
void ChangeToLower(string& str){
for (auto &c : str)
c = tolower(c);
}
int main(){
cout << "请输入一个字符串:" << endl;
string str;
cin >> str;
if (HasUpper(str)){
ChangeToLower(str);
cout << "转换后的字符串是:" << str << endl;
}
else{
cout << "该字符串不含大写字母,无需转换!" << endl;
}
return 0;
}
同じではありません。最初のものは実際のパラメータの内容を変更する必要はなく、渡されるパラメータは非定数または定数 (トップレベル const) である可能性があるため、定数参照型を次のパラメータに使用する必要があります。仮パラメータ。
2 番目のパラメータは実際のパラメータの内容を変更する必要があり、非定数参照として設定する必要があります。
演習 6.18 : 指定された名前から関数の動作を推測して、次の関数の関数宣言を作成します。
(a) ブール値を返す Compare という関数。その 2 つのパラメーターは行列クラスへの参照です。
(b) change_val という名前の関数は、vector の反復子を返し、2 つのパラメーターを取ります。1 つは int で、もう 1 つは Vector の反復子です。
答え:
//(a)
bool compare(const matrix&, matrix&);
//(b)
vector<int>::iterator change_val(int, vector<int>::iterator);
演習 6.18 : 次の宣言を考慮して、どの呼び出しが正当で、どの呼び出しが正当でないかを判断してください。不正な関数呼び出しについては、その理由を説明してください。
double calc(double);
int count(const string &, char);
int sum(vector<int>::iterator, vector<int>::iterator, int);
vector<int> vec(10);
(a) calc(23.4, 55.1);
(b) count("abcda",'a');
(c) calc(66);
(d) sum(vec.begin(), vec.end(), 3.8);
回答: (a) は不正です。関数の宣言にはパラメータが 1 つしか含まれておらず、関数の呼び出しには 2 つのパラメータが指定されているため、コンパイルできません。
(b) は正当であり、文字列リテラルは定数参照を初期化できます。文字リテラルは文字変数を初期化できます。
(c) は正当であり、int は double に変換できます。
(d) は正当であり、vec.begin() と vec.end() の型は両方とも仮パラメータで必要な Vector::iterator であり、3 番目の実パラメータ 3.8 は自動的に仮パラメータで必要な int 型に変換できます。パラメータ。
演習 6.20 : 参照パラメータを const 参照にする必要があるのはどのような場合ですか? 仮パラメータは定数参照である必要があるのに、それを通常の参照にするとどうなるでしょうか?
回答: パラメーターの内容を変更する必要がある場合は、通常の参照型として設定します。それ以外の場合、パラメーターの内容を変更する必要がない場合は、定数参照型として設定するのが最善です。
定数参照である必要がある仮パラメータを通常の参照型に設定すると、いくつかの問題が発生する可能性があります。
1 つはユーザーを誤解させやすいこと、つまりプログラムでは実パラメータの内容を変更できること、もう 1 つは関数が
受け入れることができる実パラメータのタイプが制限されており、const オブジェクトやリテラル定数を渡すことができないことです。 、または通常の への型変換が必要なオブジェクト。 の参照パラメータ。
2.4 配列パラメータ
配列はコピーできません。配列を使用する場合はポインタに変換されます。したがって、配列を関数に渡すときは、配列の最初の要素へのポインタを渡します。
配列を値で渡すことはできませんが、仮パラメータは配列のように記述することができます。
// 尽管形式不同,但这三个print函数是等价的
// 每个函数都有一个const int*类型的形参
void print(const int*);
void print(const int[]); // 可以看出来,函数的意图是作用于一个数组
void print(const int[10]); // 这里的维度表示我们期望数组含有多少元素,实际不一定
int i = 0, j[2] = {
0, 1};
print(&i); // 正确:&i的类型是int*
print(j); // 正确: j转换成int*并指向j[0]
配列はポインターの形式で関数に渡されるため、関数は最初は配列の正確なサイズを知りません。呼び出し元はこれに関する追加情報を提供する必要があります。ポインター パラメーターを管理するには、次の 3 つの一般的な手法があります。
- フラグを使用して配列の長さを指定する
最初の方法では、配列自体に終了マーカーを含めます。C スタイルの文字列は、最後の null 文字でマークされます。
- 標準ライブラリ仕様を使用する
2 番目の方法は、配列の終端へのポインターを使用し、end()
配列の終端ポインターを取得するために使用できます。
- 配列のサイズを表す仮パラメータを明示的に渡します。
3 番目の方法では、数値を使用して配列のサイズを表します。
配列パラメータと定数
関数が配列要素に対して書き込み操作を実行する必要がない場合は、配列パラメーターを const へのポインターとして定義する必要があります。関数が実際に要素の値を変更したい場合にのみ、仮パラメータは非定数へのポインタとして定義されます。
配列参照パラメータ
仮パラメータは配列への参照にすることができ、その参照は対応する実パラメータにバインドされます。つまり、配列にバインドされます。
//正确: 形参是数组的引用,维度是类型的一部分
void print(int (&arr) [10]) {
for (auto elem : arr)
cout << elem << endl;
}
// &arr两端的括号必不可少:
// f(int &arr[10]) // 错误:将arr声明成了引用的数组
// f(int (&arr)[10]) // 正确:arr是具有10个整数的整型数组的引用
多次元配列を渡す
多次元配列を関数に渡す場合、実際に渡されるのは配列の最初の要素へのポインタです。配列の 2 番目の次元 (および後続のすべての次元) のサイズは配列型の一部であり、省略できません。
// matrix指向数组的首元素,该数组的元素是由10个整数构成的数组
void print(int (*matrix)[10], int rowSize) {
/* ... */ }
// *matrix两端的括号必不可少:
int *matrix[10]; // 10个指针构成的数组
int (*matrix)[10]; // 指向含有10个整数的数组的指针
// 等价定义
void print(int matrix[][10], int rowSize) {
/* ... */ }
演習 6.21 : 2 つのパラメータを受け取る関数を作成します。1 つは int 型の数値で、もう 1 つは int ポインタです。この関数は、int の値とポインターが指す値を比較し、大きい方を返します。この関数のポインタの型は何にすべきでしょうか?
回答: この関数は実際に、最初の引数の値と 2 番目の引数が指す配列の最初の要素の値を比較します。両方のパラメータの内容は変更されないため、ポインタの型は const int* である必要があります。
#include <iostream>
#include <string>
#include <ctime>
using namespace std;
int myCompare(const int val, const int *p){
return (val > *p) ? val : *p;
}
int main(){
srand((unsigned)time(NULL));
int a[10];
for (auto &i : a)
i = rand() % 100;
cout << "请输入一个数:";
int j;
cin >> j;
cout << "您输入的数与数组首元素中较大的是:" << myCompare(j, a) << endl;
cout << "数组的全部元素是:" << endl;
for (auto i : a)
cout << i << " ";
cout << endl;
return 0;
}
演習 6.22 : 2 つの int ポインタを交換する関数を作成します。
回答:
2 つの int ポインタを交換するとは、2 つのポインタの値を交換すること、つまり、ポインタが指すメモリ アドレスを交換することになります。
#include<iostream>
#include<typeinfo>
using namespace std;
//使用指针的引用从而改变指针的值。
int Swap(int *&a, int *&b){
int *temp = a;
a = b;
b = temp;
}
int main(){
int a{
1}, b{
2};
int *ap = &a, *bp = &b;
cout << ap << " " << bp << endl;
Swap(ap, bp); //不能直接传入 &a,&b 因为这两个是 a b 的地址,是常量。
cout << ap << " " << bp << endl;
cout << "a = " << *ap << '\n' << "b = " << *bp << endl;
return 0;
}
演習 6.23 : このセクションで紹介したいくつかの印刷関数を参照し、理解に基づいて独自のバージョンを作成します。
各関数は、以下に定義されているように、入力 i および j に対して順番に呼び出されます。
回答:
print 関数の 3 つのバージョンが実装されています。
最初のバージョンはポインターの境界を制御しません。2
番目のバージョンは呼び出し元によって配列の次元を指定します。3
番目のバージョンは C で新たに指定された begin 関数と end 関数を使用します。 ++11. 配列の境界。
#include <iostream>
using namespace std;
// 参数是常量整型指针
void print1(const int *p){
cout << *p << endl;
}
// 参数有两个,分别是常量整型指针和数组的容量
void print2(const int *p, const int sz){
int i = 0;
while (i != sz) {
cout << *p++ << endl;
++i;
}
}
// 参数有两个,分别是数组的首尾边界
void print3 (const int *b, const int *e){
for (auto q = b; q != e; ++q){
cout << *q << endl;
}
}
int main(){
int i = 0, j[2] = {
0, 1 };
print1(&i);
print1(j);
cout << endl;
print2(&i, 1);
//计算得到数组j的容量
print2(j, sizeof(j) / sizeof(*j));
cout << endl;
auto b = begin(j);
auto e = end(j);
print3(b, e);
return 0;
}
演習 6.24 : 次の関数の動作を説明してください。コードに問題がある場合は、指摘して修正してください。
void print(const int ia[10])
{
for (size_t i = 0; i != 10; ++i)
cout << ia[i] << endl;
}
回答: rint 関数の定義には潜在的なリスクがあります。つまり、受信配列の次元は 10 であると予想されますが、実際には任意の次元の配列を渡すことができます。受信配列の次元が大きい場合、print 関数はエラーを発生させることなく配列の最初の 10 要素を出力します。逆に、受信配列の次元が 10 未満の場合、print 関数は未定義の要素を強制的に出力します。価値観。
void print(const int ia[], const int sz)
{
for (size_t i = 0; i != sz; ++i)
cout << ia[i] << endl;
}
2.5 main: コマンドラインオプションの処理
コマンドラインで次の形式で main 関数にパラメータを渡すことができます。
int main(int argc, char *argv[]) {
/*...*/ }
int main(int argc, char **argv) {
/*...*/ }
- argc は配列内の文字列の数を表します
- argv は、要素が C スタイルの文字列へのポインターである配列です。
argv で引数を使用する場合は、オプションの引数が argv[1] で始まることに注意してください。argv[0] にはユーザー入力ではなくプログラムの名前が保持されます。
演習 6.25 : 2 つの引数を取る main 関数を作成します。実パラメータの内容を文字列オブジェクトに連結して出力します。
答え:
#include<iostream>
#include<string>
using namespace std;
int main(int argc, char **argv){
string result{
};
for(int i = 1;i != argc; ++i){
result += argv[i];
}
cout << result << endl;
return 0;
}
演習 6.26 : このセクションで示されているオプションを受け入れ、main 関数に渡される実際の引数を出力するプログラムを作成してください。
回答:
正直に言うと、この質問の意味がわかりません。
#include <iostream>
using namespace std;
int main(int argc, char **argv){
for (int i = 1; i != argc; ++i){
cout << "argc[" << i << "]:" << argv[i] << endl;
}
return 0;
}
2.6 可変パラメータを持つ関数
新しい C++11 標準では、可変実数パラメータを持つ関数を処理するための 2 つの主な方法が提供されています。
- 実際のパラメータの型が同じ場合は、initializer_list 標準ライブラリの型を使用できます。
- 引数の型が異なる場合は、可変個引数テンプレートを定義できます。
- C++ では、省略記号仮パラメータを使用して可変数の実パラメータを渡すこともできますが、この関数は通常、C 関数と交換されるインターフェイス プログラムでのみ使用されます。
initializer_list 形参
initializer_list は、ヘッダー ファイル initializer_list で定義される標準ライブラリ タイプで、特定のタイプの値の配列を表します。
次の操作を提供します。
- ベクトルと同様に、initializer_list もテンプレート型であり、オブジェクトを定義する際には、リストに含まれる要素の型を指定する必要があります。
- ベクトルとは異なり、initializer_list オブジェクト内の要素は常に定数値であり、変更できません。
次の形式を使用して、可変数の引数に作用するエラー メッセージを出力する関数を作成します。
void error_msg(initializer_list<string> il)
{
for (auto beg = il.begin(); beg != il.end(); ++beg)
cout << *beg << " " ;
cout << endl;
}
一連の値をinitializer_listパラメータに渡したい場合は、そのシーケンスを中括弧で囲む必要があります。
// expected和actual是string对象
if (expected != actual)
error_msg({
"functionX", expected, actual});
else
error_msg({
"functionX", "okay"});
次の例に示すように、initializer_list パラメーターを持つ関数は同時に他のパラメーターを持つこともでき、initializer_list の走査では range for ステートメントを使用することもできます。
void error_msg(ErrCode e, initializer_list<string> il)
{
cout << e.msg() << ": ";
for (const auto &elem : il)
cout << elem << " " ;
cout << endl;
}
省略記号パラメータ
省略記号パラメータは、C++ プログラムが varargs と呼ばれる C 標準ライブラリ関数を使用する特別な C コードにアクセスできるようにするために設定されます。一般に、省略記号パラメータは他の目的に使用しないでください。
省略記号仮パラメータは、C および C++ に共通の型にのみ使用する必要があり、ほとんどのクラス型のオブジェクトは、省略記号仮パラメータに渡すと正しくコピーできません。
省略記号パラメータは、パラメータ リストの最後にのみ指定できます。
void foo(parm_list, ...);
void foo(...);
演習 6.27 : 引数がInitializer_list 型のオブジェクトであり、その関数がリスト内のすべての要素の合計を計算する関数を作成します。
答え:
#include <iostream>
#include <initializer_list>
using namespace std;
int iCount(initializer_list<int> il){
int count = 0;
// 遍历il的每一个元素
for (const auto &val : il)
count += val;
return count;
}
int main(){
// 使用列表初始化的方式构建initializer_list<int>对象
// 然后把它作为实参传递给函数iCount
cout << "sum of 1,6,9:" << iCount({
1, 6, 9 }) << endl;
cout << "sum of 4,5,9,18: " << iCount({
4, 5, 9, 18 }) << endl;
cout << "sum of 10,10,10,10,10,10,10,10,10: "
<< iCount({
10, 10, 10, 10, 10, 10, 10, 10, 10 }) << endl;
return 0;
}
演習 6.28 : error_msg 関数の 2 番目のバージョンに ErrCode 型のパラメーターを含めます。ループ内の elem の型はどこにありますか?
回答:const string &。
演習 6.29 : スコープ付き for ループで initializer_list オブジェクトを使用する場合、ループ制御変数を参照型として宣言する必要がありますか? なぜ?
回答: 参照型の利点は、参照されるオブジェクトを直接操作でき、より複雑な型のオブジェクトやコンテナ オブジェクトのコピーを回避できることです。initializer_list オブジェクトの要素は常に定数値であるため、参照型を設定してループ制御変数の内容を変更することはできません。
initializer_list のオブジェクト型がクラス型またはコンテナ型(文字列など)の場合のみ、ループの範囲のループ制御変数を参照型に設定する必要があります。
つまり、initializer_list 内のオブジェクトは変更できません。
3. 戻り値の型と戻り値ステートメント
return ステートメントは、現在実行中の関数を終了し、関数が呼び出された場所に制御を返します。これには 2 つの形式があります。
return;
return expression;
3.1 戻り値のない関数
戻り値のないReturnは、戻り値の型がvoidの関数で使用されます。明示的な return ステートメントがない場合は、最後の行が暗黙的に戻ります。
プログラムを途中で終了したい場合は、return を使用できます。
3.2 戻り値のある関数
関数の戻り値の型が void でない限り、関数内の各 return ステートメントは値を返す必要があり、戻り値の型は関数の戻り値の型と同じであるか、暗黙的に関数の戻り値の型に変換できる必要があります。関数の戻り値の型 ( main 関数を除く)。
返品に関しての注意点は以下の2点です。
- 値を返さない return ステートメントはエラーであり、コンパイラーはこのエラーを検出できます。
- return ステートメントを含むループの後にも return ステートメントが存在する必要があります。そうでない場合、プログラムは間違っていますが、多くのコンパイラはこのエラーを捕捉できません。
値がどのように返されるか
関数は、変数またはパラメーターを初期化するのとまったく同じ方法で値を返します。返された値は、関数呼び出しの結果である呼び出しサイトでの一時変数を初期化するために使用されます。関数が参照型を返す場合、その参照は参照するオブジェクトの単なるエイリアスです。
string make_plural(size_t ctr, const string &word, const string &ending){
return (ctr > 1) ? word + ending : word;
}
この関数の戻り値は、単語のコピーまたは一時文字列オブジェクトのいずれかです。
const string &shorterString(const string &s1, const string &s2){
return s1.size() <= s2.size() ? s1:s2;
}
この関数は、コピーされない 2 つの引数のうちの 1 つへの参照を返します。
ローカル オブジェクトへの参照やポインタを返さない
関数が完了すると、関数内の記憶領域も解放されるため、関数内のローカル変数の参照を返すことはできません。
// 严重错误: 这个函数试图返回局部对象的引用
const string &manip()
{
string ret;
// 以某种方式改变一下ret
if (!ret.empty())
return ret; // 错误:返回局部对象的引用!
else
return "Empty"; // 错误:"Empty"是一个局部临时量
}
ローカル オブジェクトへの参照を返すことは間違いであり、ローカル オブジェクトへのポインターを返すことも間違いであり、一時パラメーターまたは実際のパラメーターへの参照を初期化することによってのみ返すことができます。
参照は左辺値を返します
参照を返す関数を呼び出すと左辺値が取得され、他の型を呼び出すと右辺値が取得されます。参照を返す関数呼び出しは、代入を含め、他の左辺値と同様に使用できます。
char &get_val(string &str, string::size_type ix)
{
return str[ix]; // get_val assumes the given index is valid
}
int main()
{
string s("a value");
cout << s << endl; // prints a value
get_val(s, 0) = 'A'; // changes s[0] to A
cout << s << endl; // prints A value
return 0;
}
しかし、定数参照を返す場合、それに値を割り当てることはできず、誰もが真実を理解します。
リスト初期化子の戻り値
C++11 では、関数が中括弧で囲まれた値のリストを返すことができると規定しています。他の戻り値の型と同様に、リストは関数呼び出しの結果を表す一時変数を初期化するために使用されます。リストが空の場合、一時関数は値の初期化を実行します。それ以外の場合、戻り値は関数の戻り値の型によって決まります。
- 関数が組み込み型を返す場合、リストには最大でも 1 つの値が含まれ、その値はターゲットの型以上のスペースを占有してはなりません。
- 関数がクラス型を返す場合、初期値の使用方法はクラス自体が定義します。
メイン関数mainの戻り値
関数の戻り値の型が void でない場合は、main 関数が return ステートメントなしで終了する場合を除き、値を返さなければなりません。制御フローが main 関数の最後に達し、return ステートメントがない場合、コンパイラは 0 を返す return ステートメントを暗黙的に挿入します。
main 関数の戻り値はステータス インジケータとして見ることができます。0 が返された場合は実行が成功したことを意味し、他の値が返された場合は実行が失敗したことを意味します。ゼロ以外の値の具体的な意味はマシンによって異なります。
main 関数の戻り値をマシンから独立させるために、ヘッダー ファイル cstdlib は、それぞれ実行の成功と失敗を表す 2 つの前処理変数 EXIT_SUCCESS と EXIT_FAILURE を定義します。
int main()
{
if (some_failure)
return EXIT_FAILURE; // 定义在cstdlib头文件中
else
return EXIT_SUCCESS; // 定义在cstdlib头文件中
}
これらはプリプロセッサ変数であるため、前に std:: を付けることも、using ステートメントに含めることもできません。
再帰
関数が直接的または間接的にそれ自体を呼び出す場合、その関数は再帰。
// 计算val!,即1 * 2 * 3 . . . * val
int factorial(int val)
{
if (val > 1)
return factorial(val-1) * val;
return 1;
}
再帰関数では、再帰呼び出しを含まない特定のパスが必要です。そうでない場合、関数はプログラムのスタック領域がなくなるまで再帰的に実行されます。
再帰はループ反復よりも効率が劣りますが、場合によっては再帰を使用するとコードの可読性が向上します。
ループ反復は線形問題 (リンク リストなど、各ノードに一意の先行ノードと一意の後続ノードがある) の処理に適しており、再帰は非線形問題 (
ツリーなど、各ノードに一意でないノードがある) の処理に適しています。前任者と後継者)。
演習 6.30 : str_subrange 関数 (200 ページ) をコンパイルし、コンパイラが関数内のエラーをどのように処理するかを確認します。
答え:
#include <iostream>
using namespace std;
bool str_subrange(const string &str1, const string &str2){
if (str1.size() == str2.size())
return str1 == str2;
auto size = (str1.size() < str2.size()) ? str1.size() : str2.size();
for (decltype(size)i = 0; i != size; ++i){
if (str1[i] != str2[i])
return;
}
}
演習 6.31 : 返された参照が無効になるのはどのような場合ですか? 返される定数への参照が有効になるのはどのような場合ですか?
回答:
参照が関数内のローカル変数を参照している場合、その参照は無効ですが、参照先のオブジェクトが関数呼び出しの前に存在する場合、定数参照は有効です。
演習 6.32 : 次の関数は正当ですか? 正当な場合はその機能を説明し、正当でない場合はエラーを修正し、その理由を説明します。
int &get(int *array, int index) {
return array[index]; }
int main()
{
int ia[10];
for (int i = 0; i != 10; ++i)
get(ia, i) = i;
}
答え:
get 関数は、整数配列の最初の要素を実際に指す整数ポインターを受け入れます。また、配列内の要素のインデックス値を表す整数も受け入れます。戻り値の型は整数参照であり、参照されるオブジェクトは配列配列の要素です。get 関数の実行が終了すると、呼び出し元は、実際のパラメーター配列 arry 内のインデックスが Index である要素への参照を取得します。
main 関数では、まず 10 個の整数を含む配列を作成します。名前は ia です。なお、iaは関数内で定義されているためデフォルトの初期化は行われず、このときiaの各要素の値を直接出力した場合、それらの値は不定となります。
次にループに入ります。各サイクルでは、get 関数を使用して配列 ia の i 番目の要素の参照を取得し、値 i をその参照に割り当てます。つまり、値 i を i 番目の要素に割り当てます。ループの最後では、ia の要素に 0 から 9 までの値が順番に割り当てられます。
#include <iostream>
using namespace std;
int &get(int *array, int index) {
return array[index]; }
int main()
{
int ia[10];
for (int i = 0; i != 10; ++i)
get(ia, i) = i;
for (int i = 0; i < 10; i++)
{
printf("%d ", ia[i]);
}
printf("\n");
return 0;
}
演習 6.33 : ベクトル オブジェクトの内容を出力する再帰関数を作成します。
答え:
#include <iostream>
#include <vector>
using namespace std;
void print(vector<int> vInt, unsigned index){
unsigned sz = vInt.size();
if (!vInt.empty() && index < sz){
cout << vInt[index] << endl;
print(vInt, index + 1);
}
}
int main()
{
vector<int> v{
1, 3, 5, 7, 9, 11, 13, 15 };
print(v, 0);
return 0;
}
演習 6.34 : 階乗関数の停止条件が次の場合はどうなるでしょうか?if (val != 0)
回答: 再帰関数のパラメータの型が int の場合、理論的には、ユーザーが階乗関数に渡すパラメータは負の数になる可能性があります。元のプログラムのロジックによれば、パラメータが負の数の場合、関数の戻り値は 1 になります。
再帰関数の停止条件を変更すると、パラメータの値が負の場合、1 つずつ再帰してオーバーフローするまで連続乗算演算を実行します。したがって、if 文の条件を上記の形式に変更することはできません。
演習 6.35 : 階乗関数を呼び出すときに、val– ではなく val-1 という値を渡すのはなぜですか?
回答: 渡された値 val-1 が val- に変更されると、予期しない状況が発生します。つまり、変数のデクリメント操作と変数値の読み取り操作が同じ式内に共存します。 、プラス記号 演算子も評価順序を指定しないため、未定義の値が生成される可能性があります。
3.3 配列ポインタを返す
配列はコピーできないため、関数は配列を返すことはできませんが、配列へのポインタまたは参照を返すことはできます。
返される配列ポインターまたは参照は、型エイリアスを使用して簡略化できます。
typedef int arrT[10]; // arrT is a synonym for the type array of ten ints
using arrtT = int[10]; // equivalent declaration of arrT; see § 2.5.1 (p. 60)
arrT* func(int i); // func returns a pointer to an array of five ints
func は 10 個の整数の配列へのポインタを返します。
配列ポインタを返す関数を宣言する
まず、配列へのポインターがどのように定義されているかを見てみましょう。
int arr[10];
int (*p)[10] = &arr;
まず、p がポインタであるかっこの内側を見て、次に両側を見て、p が 10 個の整数の配列へのポインタであることを示します。
同様に、配列ポインターを返す関数は次の形式になります。
Type (*function(parameter_list))[dimension]
このうち、Type は要素の型を示し、dimension は配列のサイズを示し、(*function (parameter_list)) の両端の括弧が存在する必要があり、そのような括弧のペアが存在しない場合、関数の戻り値の型は次のようになります。ポインタの配列になります。
型エイリアスのない func 関数:
int (*func(int))[10];
// 可以按下面解释理解,也可以对照 p 的定义理解。
// func(int i)表示调用func函数时需要一个int类型的实参
// (*func(int i))意味着可以对函数调用的结果执行解引用操作
// (*func(int i))[10]表示解引用func的调用将得到一个大小是10的数组
// int(*func(int i))[10]表示数组中的元素是int类型
末尾を使用して型を返す
末尾の戻り値の型によりfunc の宣言が簡素化されます。どの関数でも tail-to-return を使用できますが、これは複雑な戻り値の型を持つ関数に有効です。
end-to-return 型は仮パラメータリストに従い、-> で始まります。関数の実際の戻り値の型が仮パラメータ リストの後にあることを示すには、戻り値の型が表示される場所に auto キーワードを追加する必要があります。
// func接受一个int类型的实参,返回一个指针,该指针指向含有10个整数的数组
auto func(int i) -> int(*)[10];
decltypeを使用する
関数によって返されたポインターがどの配列を指すかがわかっている場合は、decltype キーワードを使用して戻り値の型を宣言できます。ただし、decltype は配列型をポインター型に変換しないため、関数宣言に * 記号を追加する必要があります。
int odd[] = {
1,3,5,7,9};
int even[] = {
0,2,4,6,8};
// 返回一个指针,该指针指向含有5个整数的数组
decltype(odd) *arrPtr(int i)
{
return (i % 2) ? &odd : &even; // 返回一个指向数组的指针
}
演習 6.36 : 10 個の文字列オブジェクトを含む配列への参照を返す関数宣言を作成します。末尾の戻り値の型、decltypes、または型の別名を使用しないでください。
答え:
string (&func())[10];
演習 6.37 :
答え:
using arrofstring= string [10];
arrofstring &func();
auto func -> string (&)[10];
string arr[10];
decltype(arr) &func();
演習 6.38 : 配列への参照を返すように arrPtr 関数を変更します。
int odd[] = {
1,3,5,7,9};
int even[] = {
0,2,4,6,8};
// 返回一个指针,该指针指向含有5个整数的数组
decltype(odd) *arrPtr(int i)
{
return (i % 2) ? &odd : &even; // 返回一个指向数组的指针
}
答え:
int odd[] = {
1,3,5,7,9};
int even[] = {
0,2,4,6,8};
decltype(odd) &arrPtr(int i)
{
return (i % 2) ? odd : even; // 返回一个指向数组的指针
}
4. 関数のオーバーロード
同じ名前であり、同じスコープ内にある異なるパラメーター リストを持つ複数の関数は、オーバーロード。
void print(const char *cp);
void print(const int *beg, const int *end);
void print(const int ia[), size_t ze);
コンパイラは、渡された引数の型に基づいて、どの関数を使用するかを推測します。
int j[2] = {
0,1};
print("Hello World"); // calls print(const char*)
print(j, end(j) - begin(j)); // calls print(const int*, size_t)
print(begin(j), end(j)); // calls print(const int*, const int*)
オーバーロードされた関数を定義する
2 つの関数は、戻り値の型を除くすべての点で同一であってはなりません。
Record lookup(const Account&);
bool lookup(const Account&); // 错误:与上一个函数相比只有返回类型不同
2 つの仮パラメータの型が異なるかどうかを判断する
異なる仮パラメータはオーバーロードと呼ばれ、同じことを繰り返し定義します。
// each pair declares the same function
Record lookup(const Account &acct);
Record lookup(const Account&); // parameter names are ignored
typedef Phone Telno;
Record lookup(const Phone&);
Record lookup(const Telno&); // Telno and Phone are the same type
オーバーロードとconstパラメータ
トップレベルの const は、関数に渡されるオブジェクトには影響しません。また、トップレベルの const を含む仮パラメータは、トップレベルの const を持たない別のパラメータと区別できません。
Record lookup(Phone);
Record lookup(const Phone); // 重复声明了Record lookup(Phone)
Record lookup(Phone*);
Record lookup(Phone* const); // 常量指针,重复声明了Record lookup(Phone*)
如果形参是某种类型的指针或引用,则通过区分其指向的对象是常量还是非常量可以实现函数重载,此时的const是底层的。
// 对于接受引用或指针的函数来说,对象是常量还是非常量对应的形参不同
// 定义了4个独立的重载函数
Record lookup(Account&); // 函数作用于Account的引用
Record lookup(const Account&); // 新函数,作用于常量引用
Record lookup(Account*); // 新函数,作用于指向Account的指针
Record lookup(const Account*); // 新函数,作用于指向常量的指针
const_cast 和 重载
const_cast 可以用于函数的重载。
当函数的实参是常量时,返回的结果仍然是常量的引用。
// 比较两个string对象的长度,返回较短的那个引用
const string &shorterString(const string &s1, const string &s2)
{
return s1.size() <= s2.size() ? s1 : s2;
}
当函数的实参不是常量时,将得到普通引用。
string &shorterString(string &s1, string &s2)
{
auto &r = shorterString(const_cast<const string&>(s1),
const_cast<const string&>(s2));
return const_cast<string&>(r);
}
调用重载的函数
函数匹配(function matching) 也叫做 重载确定(overload resolution),是指编译器将函数调用与一组重载函数中的某一个进行关联的过程。
编译器首先将调用的实参与重载集合中每一个函数的形参进行比较,然后根据比较的结果决定到底调用是哪个函数。
调用重载函数时有三种可能的结果:
- 编译器找到一个与实参 最佳匹配(best match) 的函数,并生成调用该函数的代码。
- 编译器找不到任何一个函数与实参匹配,发出 无匹配(no match) 的错误信息。
- 有一个以上的函数与实参匹配,但每一个都不是明显的最佳选择,此时编译器发出 二义性调用(ambiguous call) 的错误信息。
练习 6.39:说明在下面的每组声明中第二条语句是何含义。如果有非法的声明,请指出来。
(a) int calc(int, int);
int calc(const int, const int);
(b) int get();
double get();
(c) int *reset(int *);
double *reset(double *);
答:(a)的第二个声明是非法的。它的意图是声明另外一个函数,该函数只接受整型常量作为实参,但是因为 顶层 const 不影响传入函数的对象,所以一个拥有顶层 const 的形参无法与另一个没有顶层 const 的形参区分开来。
(b)的第二个声明是非法的。它的意图是通过函数的返回值区分两个同名的函数,但是这不可行,因为 C++规定重载函数必须在形参数量或形参类型上有所区别。如果两个同名函数的形参数量和类型都一样,那么即使返回类型不同也不行。
(c)的两个函数是重载关系,它们的形参类型有区别。
4.1 重载与作用域
在内层作用域声明的名字,将覆盖外层的同名实体。
string read();
void print(const string &);
void print(double); // 重载print函数
void fooBar(int ival)
{
bool read = false; // 新作用域:隐藏了外层的read
string s = read(); // 错误:read是一个布尔值,而非函数
// 不好的习惯:通常来说,在局部作用域中声明函数不是一个好的选择
void print(int); // 新作用域:隐藏了之前的print
print("Value: "); // 错误:print(const string &)被隐藏掉了
print(ival); // 正确:当前print(int)可见
print(3.14); // 正确:调用print(int); print(doub1e)被隐藏掉了
}
5、特殊用途语言特性
5.1 默认实参
默认实参 作为形参的初始值出现在形参列表中。可以为一个或多个形参定义默认值,不过一旦某个形参被赋予了默认值,它后面的所有形参都必须有默认值。
typedef string::size_type sz;
string screen(sz ht = 24, sz wid = 80, char backgrnd = ' ');
使用默认实参调用函数
调用含有默认实参的函数时可以省略该实参:
string window;
window = screen(); // equivalent to screen(24,80,' ')
window = screen(66);// equivalent to screen(66,80,' ')
window = screen(66, 256); // screen(66,256,' ')
window = screen(66, 256, '#'); // screen(66,256,'#')
默认参数负责填补函数调用缺少的尾部实参(右侧的),下面的第一个方法省略头部的就会出错,第二个将字符的ascll值传给第一个参数,后两个默认初始化。
window = screen( , , '?');
window = screen('?');
默认实参声明
虽然多次声明同一个函数是合法的,但是同一作用域同一函数的声明的一个形参只能被赋予一次默认实参:
// 表示高度和宽度的形参没有默认位
string screen(sz, sz, char = ' ');
string screen(sz, sz, char = '*'); // 错误:重复声明
string screen(sz = 24, sz = 80, char); // 正确:添加默认实参
默认实参初始值
局部变量不能作为函数的默认实参。能转换成形参所需类型的表达式就能作为默认实参。
// wd、def和ht的声明必须出现在函数之外
sz wd = 80;
char def = ' ';
sz ht();
string screen(sz = ht(), sz = wd, char = def);
string window = screen(); // 调用screen(ht(), 80,' ')
void f2()
{
def = '*'; // 改变默认实参的值
sz wd = 100; // 隐藏了外层定义的wd,但是没有改变默认值
window = screen(); // 调用screen(ht(), 80, '*')
}
用作默认实参的名字在函数声明所在的作用域内解析,这些名字的求值过程发生在函数调用时。比如 f2() 里面的def,f2() 内的 wd 与 screen 的默认参数无关,所以没生效。
练习 6.40:下面的哪个声明是错误的?为什么?
(a) int ff(int a, int b = 0, int c = 0);
(b) char *init(int ht = 24, int wd, char bckgrnd);
答:a正确,b错误,因为默认实参只能位于形参列表的尾部。
练习 6.41:下面的哪个调用是非法的?为什么?哪个调用虽然合法但显然与程序员的初衷不符?为什么?
char *init(int ht, int wd = 80, char bckgrnd = ' ');
(a) init();
(b) init(24,10);
(c) init(14,'*');
答:a不正确,至少需要传入一个形参对第一个参数初始化。
b正确,两个参数对前两个形参初始化。
c正确,语法上是合法的,但是与程序的原意不符。从语法上来说,第一个实参对应第一个形参 ht,第二个实参的类型虽然是 char,但是它可以自动转换为第二个形参 wd 所需的 int 类型,所以编译时可以通过,但这显然违背了程序的原意,正常情况下,字符 * 应该被用来构成 bckgrnd。
练习 6:42:给 make_plural 函数的第三个形参赋予默认实参’s’, 利用新版本的函数输出单词 success 和 failure 的单数和复数形式。
答:
#include<iostream>
#include<string>
using namespace std;
// 最后一个形参赋予了默认实参
string make_plural(size_t ctr, const string &word, const string &ending = "s")
{
return (ctr > 1) ? word + ending : word;
}
int main(){
cout << "success的单数形式是:" << make_plural(1, "success", "es") << endl;
cout << "success的复教形式是:" << make_plural(2, "success", "es") << endl;
// 一般情况下调用该函数只需要两个实参
cout << "failure的单数形式是:" << make_plural(1, "failure") << endl;
cout << "failure的单数形式是:" << make_plural(2, "failure") << endl;
return 0;
}
5.2 内联函数和 constexpr 函数
内联函数可避免函数调用的开销
内联函数会在每个调用点上“内联地”展开,省去函数调用所需的一系列工作。
定义内联函数时需要在函数的返回类型前添加关键字 inline。
// 内联版本:寻找两个string对象中较短的那个
inline const string &
shorterString(const string &s1, const string &s2)
{
return s1.size() <= s2.size() ? s1 : s2;
}
一般来说,内联机制适用于优化规模较小、流程直接、调用频繁的函数。内联函数中不允许有循环语句和 switch 语句,否则函数会被编译为普通函数。
constexpr 函数
constexpr 函数是指能用于常量表达式的函数。constexpr 函数的返回类型及所有形参的类型都得是字面值类型。函数体内只能有一条return语句:
constexpr int new_sz() {
return 42; }
constexpr int foo = new_sz(); // ok: foo is a constant expression
constexpr 函数的返回值可以不是一个常量。
// 如果arg是常量表达式,则scale(arg)也是常量表达式
constexpr size_t scale(size_t cnt)
{
return new_sz() * cnt;
}
int arr[scale(2)]; //正确:scale(2)是常量表达式
int i = 2; //i 不是常量表达式
int a2[scale(i)]; //错误:scale(i) 不是常量表达式。
和其他函数不同,内联函数和 constexpr 函数可以在程序中多次定义。因为在编译过程中,编译器需要函数的定义来随时展开函数,仅有函数的声明是不够的。对于某个给定的内联函数或 constexpr 函数,它的多个定义必须完全一致。因此内联函数和 constexpr 函数通常定义在头文件中。
练习 6.43:你会把下面的哪个声明和定义放在头文件中?哪个放在源文件中?为什么?
(a) inline bool eq(const BigInt&, const BigInt&) {
...}
(b) void putValues(int *arr, int size);
答:a 应该放到头文件中,在编译过程中,编译器需要函数的定义来随时展开函数,仅有函数的声明是不够的。对于某个给定的内联函数或 constexpr 函数,它的多个定义必须完全一致。因此内联函数和 constexpr 函数通常定义在头文件中。
b 是函数声明,放到头文件中。
练习 6.44:将6.2.2节的isShorter函数改写成内联函数。
答:
inline bool isShorter(const string &s1, const string &s2)
{
return s1.size() < s2.size();
}
练习 6.45:回顾在前面的练习中你编写的那些函数,它们应该是内联函数吗?如果是,将它们改写成内联函数;如果不是,说明原因。
答:在本章前面实现的函数中,大多规模较小且流程直接,适合于设置为内联函数;如果以后遇到一些代码行数较多的函数,就不适合了。
练习6.11中的 reset 函数改写后的形式是:
inline void reset(int &i)
{
i=0;
}
练习 6.46:能把isShorter函数定义成constexpr函数吗?如果能,将它改写成constxpre函数;如果不能,说明原因。
答:constexpr 函数是指能用于常量表达式的函数,constexpr 函数的返回类型和所有形参的类型都得是字面值类型,而且函数体中必须有且只有一条 return 语句。
显然 isShorter 函数不符合 constexpr 函数的要求,它虽然只有一条 return 语句,但是返回的结果调用了标准库 string 类的 size() 函数和 < 比较符,无法构成常量表达式,因此不能改写成 constexpr 函数。
5.3 调试帮助
assert 预处理宏
assert 是一种预处理宏,定义在 cassert 头文件中。所谓预处理宏其实是一个预处理变量,它的行为有点类似于内联函数。assert 宏使用一个表达式作为它的条件:
assert(expr);
首先对 expr 求值,如果表达式为假(即0),assert 输出信息并终止程序的执行;如果
表达式为真(即非0),assert什么也不做。
因为他是预处理变量,所以既不能在前面加上 std::,也不能在using声明中出现。
NDEBUG 预处理变量
assert 的行为依赖于于一个名为 NDEBUG 的预处理变量的状态。
- 如果定义了 NDEBUG,则 assert 什么也不做;
- 默认状态下没有定义 NDEBUG,此时 assert 将执行运行时检查。
可以用 #define 定义NDEBUG,也可以是命令行选项定义预处理变量:
g++ -D NDEBUG main.c
可以使用 NDEBUG 编写自己的条件调试代码:
void print(const int ia[], size_t size)
{
#ifndef NDEBUG
// _ _func_ _ is a local static defined by the compiler that holds the function's name
cerr << _ _func_ _ << ": array size is " << size << endl;
#endif
// ...
func 是 const char 的静态数组用于输出当前函数的名字。
还有4个对调试有用的名字:
练习 6.47:改写6.3.2节练习中使用递归输出 vector 内容的程序,使其有条件地输出与执行过程有关的信息。例如,每次调用时输出 vector 对象的大小。分别在打开和关闭调试器的情况下编译并执行这个程序。
void print(vector<int> vInt, unsigned index){
unsigned sz = vInt.size();
if (!vInt.empty() && index < sz){
cout << vInt[index] << endl;
print(vInt, index + 1);
}
}
答:
#include <iostream>
#include <vector>
using namespace std;
void print(vector<int> vInt, unsigned index){
#ifndef NDEBUG
cout << "length: " << vInt.size() << endl;
#endif
size_t sz = vInt.size();
if(!vInt.empty() && index < sz){
cout << vInt[index] << endl;
print(vInt, index+1);
}
}
int main()
{
vector<int> v = {
1, 3, 5, 7, 9};
print(v, 0);
return 0;
}
练习 6.48:说明下面这个循环的含义,它对assert的使用合理吗?
string s;
while (cin >> s && s != sought) {
} // 空函数体
assert(cin);
答:该程序对 assert 的使用有不合理之处。
程序执行到 assert 的原因可能有两个,一是用户终止了输入,二是用户输入的内容正好与 sought 的内容一样。如果用户尝试终止输入(事实上用户总有停止输入结束程序的时候),则 assert 的条件为假,输出错误信息,这与程序的原意是不相符的。
6、函数匹配
确定候选函数与可行函数
函数实参类型与形参类型越接近,它们匹配得越好。
重载函数集中的函数称为 候选函数(candidate function):
- 一是与被调用的函数同名;
- 二是其声明在调用点可见。
可行函数(viable function):
- 一是形参数量与函数调用所提供的实参数量相等;
- 二是每个实参的类型与对应的形参类型相同,或者能转换成形参的类型。
如果没找到可行函数,编译器讲报告无匹配函数的错误。
调用重载函数时应该尽量避免强制类型转换。
练习 6.49:什么是候选函数?什么是可行函数?
答:当程序中存在多个同名的重载函数时,编译器需要判断调用的是其中哪个函数,这时就有了候选函数和可行函数两个概念。函数匹配的第一步是选定本次调用对应的重载函数集,集合中的函数称为候选函数。函数匹配的第二步是考查本次调用提供的实参,然后从候选函数中选出能被这组实参调用的函数,这些新选出的函数称为可行函数。
练习 6.50:已知有第217页对函数 f 的声明,对于下面的每一个调用列出可行函数。其中哪个函数是最佳匹配?如果调用不合法,是因为没有可匹配的函数还是因为调用具有二义性?
void f();
void f(int);
void f(int,int);
void f(double, double = 3.14);
(a) f(2.56, 42)
(b) f(42)
(c) f(42, 0)
(d) f(2.56, 3.14)
答:
(a) f(2.56, 42) //void f(double, double = 3.14) or void f(int,int) 具有二义性。
(b) f(42) //void f(int) or void f(double, double = 3.14). void f(int) best.
(c) f(42, 0) //void f(int,int) or void f(double, double = 3.14). first is best
(d) f(2.56, 3.14) //void f(int,int) or void f(double, double = 3.14). second is best
练习 6.51:编写函数 f 的4版本,令其各输出一条可以区分的消息。
验证上一个练习的答案,如果你的回答错了,反复研究本节内容直到你弄清自己错在何处。
答:
#include<iostream>
using namespace std;
void f(){
cout << "该函数无须参数" << endl;
}
void f(int){
cout << "该函数有一个整型参数" << endl;
}
void f(int, int){
cout << "该函数有两个整型参数" << endl;
}
void f(double a, double b = 3.14){
cout << "该函数有两个双精度浮点型参数" << endl;
}
int main(){
//f(2.56, 42); // 报错
f(42);
f(42, 0);
f(2.56, 3.14);
return 0;
}
6.1 实参类型转换
为了确定最佳匹配,编译器将实参类型到形参类型的转换划分成几个等级,具体如下:
所有算术类型转换的级别都一样。
如果重载函数的区别在于它们的引用或指针类型的形参是否含有底层 const,或者指针类型是否指向 const,则调用发生时编译器通过实参是否是常量来决定函数的版本。
Record lookup(Account&); // 函数的参数是Account的引用
Record lookup(const Account&); // 函数的参数是一个常量引用
const Account a;
Account b;
lookup(a); // 调用lookup(const Account&)
lookup(b); // 调用lookup(Account&)
练习 6.52:已知有如下声明:
void manip(int ,int);
double dobj;
请指出下列调用中每个类型转换的等级。
(a) manip('a', 'z');
(b) manip(55.4, dobj);
答:
a类型提升
b算术类型转换。
练习 6.53:说明下列每组声明中的第二条语句会产生什么影响,并指出哪些不合法(如果有的话)。
(a) int calc(int&, int&);
int calc(const int&, const int&);
(b) int calc(char*, char*);
int calc(const char*, const char*);
(c) int calc(char*, char*);
int calc(char* const, char* const);
答:a无影响,第二条语句中的形参是底层const,能与非常量引用区分。
b无影响,第二条语句中的形参是底层const,能与普通指针区分。
c有影响,两个函数的区别是它们的指针类型的形参本身是否是常量,属于顶层 const,根据本节介绍的匹配规则可知,向实参添加顶层 const 或者从实参中删除顶层 const 属于精确匹配,无法区分两个函数。
7、函数指针
要想声明一个可以指向某种函数的指针,只需要用指针替换函数名称即可。
// 比较两个string对象的长度
bool lengthCompare(const string &, const string &);
// pf指向一个函数,该函数的参数是两个const string的引用,返回值是bool类型
bool (*pf)(const string &, const string &); // uninitialized
*pf 两端的括号必不可少!!!如果不写这对括号,则 pf 是一个返回值为 bool 指针的函数:
// 声明一个名为pf的函数,该函数返回bool*
bool *pf(const string &, const string &);
使用函数指针
可以直接使用指向函数的指针来调用函数,无须提前解引用指针。
pf = lengthCompare; // pf指向名为lengthCompare的函数
pf = &lengthCompare; // 等价的赋位语句: 取地址符是可选的
bool b1 = pf("hello", "goodbye"); // 调用lengthCompare函数
bool b2 = (*pf)("hello", "goodbye"); // 一个等价的调用
bool b3 = lengthCompare("hello", "goodbye"); // 另一个等价的调用
指向不同函数类型的指针间不能相互转换,给函数指针赋赋值的函数类型要与函数指针类型相同。
重载函数的指针
对于重载函数,上下文必须清晰地界定到底应该选用了哪个函数,编译器通过指针类型决定函数版本,指针类型必须与重载函数中的某一个精确匹配。
void ff(int*);
void ff(unsigned int);
void (*pf1)(unsigned int) = ff; // pf1指向ff(unsigned)
void (*pf2)(int) = ff; // 错误:没有任何一个ff与该形参列表匹配
double (*pf3)(int*) = ff; // 错误:ff和pf3的返回类型不匹配
函数指针形参
和数组类似,不能定义函数的形参,但是可以定义函数指针形参。
形参看起来像函数类型,实际上是当成指针使用的:
// 第三个形参是函数类型,它会自动地转换成指向函数的指针
void useBigger(const string &s1, const string &s2, bool pf(const string &, const string &));
// 等价的声明:显式地将形参定义成指向函数的指针
void useBigger(const string &s1, const string &s2, bool (*pf)(const string &, const string &));
// 自动将函数lengthCompare转换成指向该函数的指针
useBigger(s1, s2, lengthCompare);
调用函数时可以直接把函数当做实参,它会自动转换为指针。
关键字 decltype 作用于函数时,返回的是函数类型,而不是函数指针类型。
使用类型别名和 decltype 能简化函数指针的使用:
//function
typedef bool func(const string&, const string&);
typedef decltype(lengthCompare) func2; //等价
//pointer to function
typedef bool (*pfun)(const string &, const string &);
typedef decltype(lengthCompare) *pfun2; //等价
useBigger 就可以换成如下的声明:
void useBigger(const string &s1, const string &s2, func);
void useBigger(const string &s1, const string &s2, pfun);
返回指向函数的指针
程序无法返回函数,但能返回函数指针。
使用类型别名比较简单:
//function
using f = int(int*, int);
typedef int f(int*, int);
//pointer to function
using pf = int(*)(int*, int);
typedef int (*f)(int *, int);
和函数类型的形参不一样,返回类型不会自动转换为指针,需要显示指定:
pf f1(int); //正确:f1返回指向函数的指针。
f f1(int); //错误:f1不能返回一个函数
f *f1(int); //正确:显式指定返回的是函数的指针。
不使用类型别名:
int (*f1(int))(int *, int);
类比函数指针:
int (*p)(int *, int);
将 p 换为函数的声明即可表明这个函数返回的是函数指针。
使用尾至返回类型:
auto f1(int) -> int (*) (int *, int);
**演習 6.54**: 関数の宣言を記述し、2 つの int 仮パラメータを受け入れ、戻り値の型も int に設定し、ベクトル オブジェクトを宣言し、その要素を関数へのポインタにします。
答え:
int func(int, int);
vector<decltype(func)*> fv;
演習 6.55 : 2 つの int 値に対して加算、減算、乗算、および除算の演算を実行する 4 つの関数を作成し、これらの関数へのポインタを前の質問で作成したベクトル オブジェクトに保存します。
答え:
次の質問を見てください
演習 6.56 :
答え:
#include <iostream>
#include <vector>
#include <string>
using namespace std;
int add(int a, int b)
{
return a + b;
}
int sub(int a, int b)
{
return a - b;
}
int mul(int a, int b)
{
return a * b;
}
int dev(int a, int b)
{
return a / b;
}
int main(int argc, char **argv)
{
int a = atoi(argv[1]);
int b = atoi(argv[2]);
int func(int, int);
decltype(func) *arr[4] = {
add, sub, mul, dev};
vector<decltype(func) *> fv(arr, arr + 4);
vector<string> S = {
"a + b = ", "a - b = ", "a * b = ", "a / b = "};
int i = 0;
for (auto fun : fv)
{
cout << S[i++] << fun(a, b) << endl;
}
return 0;
}
Bozai に従って迷わないように、一緒に C++ をしっかり学びましょう