C++Primer 笔记(第七章)

    技术2024-10-19  23

    函数

    函数可以看作程序员定义的操作。与内置操作符类似,每个函数都会实现一系列的计算,然后生成一个计算结果,不同的是,函数有自己的函数名,而且操作数的个数没有限制。

    函数的定义

    形式:返回类型 函数名([形参列表])

    函数调用做了两件事:

    用对应的实参初始化函数的形参,并将控制权转移给被调函数。

    函数体是一个作用域

    函数体内定义的变量只在该函数中才可以访问,即局部变量

    形参与实参

    调用函数时,所传递的实参个数必须与函数的形参个数完全相同;个数相同

    实参必须具有与形参类型相同、或者能隐式转换为形参类型的数据类型;类型相同

    函数返回类型

    可以是内置类型、类类型、复合类型和void类型,表示该函数不返回任何值,但函数必须指定返回类型;

    参数传递

    非引用形参

    通过复制对应的实参实现初始化,用实参副本初始化形参时,函数并没有访问调用所传递的实参本身,因此不会修改实参的值。例如:

    int gcd(int v1, int v2){ while(v2){ int temp = v2; v2 = v1 % v2; v1 = temp; } return v1; } //调用gcd(i,j)时,实参i,j的值不受gcd内执行的赋值操作的影响,修改的只是形参的局部副本,一旦函数执行 //结束,这些局部变量的值也就没有了

    指针形参

    将复制实参指针,形参的任何改变作用于局部副本,对实参指针的值没有改变

    void reset(int *ip){ *ip = 0; //可改变实参指向对象的值 ip = 0; //不可以改变指针的值 } //调用 int i = 3; int *ip = &i; cout<<*p<<endl; //i = 3 cout<<p<<endl; //0x001 reset(p); cout<<*p<<endl; //i = 0 cout<<p<<endl; //0x001 //若要保护实参指向的值,可定义为指向const的指针 void reset(const int *ip);

    const形参

    即可以给函数传递const实参,也可以是非const实参

    复制实参的局限性

    下列情况不适合使用复制实参:

    当需要在函数中修改实参的值当需要以大型对象作为实参传递时。对实际的应用而言,复制对象所付出的时间和存储空间代价往往很大当没有办法实现对象的复制时

    引用实参

    void swap(int &v1, int &v2){ int tmp = v2; v2 = v1; v1 = tmp; } //引用形参直接关联其所绑定的对象,而并非这些对象的副本

    使用const形参返回额外的信息

    利用const引用避免复制,可以提高代码的执行效率

    注意:如果使用引用形参的唯一目的是避免复制,应将形参定义为const引用

    bool isShort(const string &s1, const string &s2){ return s1.size() < s2.size(); } //既不复制形参,也不能使用该引用来修改实参

    更灵活的指向const的引用

    对于非const引用形参,只能与完全同类型的非const对象关联,传递右值或者具有需要转换的类型的对象都是不允许的

    string::size_type find_char(string &s, char c){ string::size_type i = 0; while (i != s.size() && s[i] != c) ++i; return i; } //不能通过字符串字面值来调用这个函数,虽然字符串字面值可以转换为string对象 if(find_char("hello", 'o')) //编译失败

    所以,应该将不需要修改的引用形参定义为const引用。普通的非const引用形参在使用时不太灵活。这样的形参即不能用const对象初始化,也不能用字面值或产生右值的表达式实参初始化。

    传递指向指针的引用

    如何定义指向指针的引用,请看下面的程序:

    void ptrswap(int *&v1, int *&v2){ int *tmp = v2; v2 = v1; v1 = tmp; } //语句int *&v1应从右至左理解:v1是一个引用,与指向int型对象的指针相关联,即v1只是传递进ptrswap函数的任意指针的别名 int main() { int i = 10; int j = 20; int *pi = &i; int *pj = &j; cout<<"交换前"<<*pi<<*pj<<endl;; ptrswap(pi,pj); //现在pi指向j pj指向i cout<<"交换后"<<*pi<<*pj<<endl; return 0; }

    vector和其他容器的形参

    通常,函数不应该有vector或其他标准库容器类型的形参。调用含有普通的非引用vector形参的函数将会复制vector的每一个元素。可以考虑将形参声明声明为引用类型。

    数组形参

    由于数组的两个特性:一是不能复制数组;二是使用数组名时,数组名自动退化为指向其第一个元素的指针。所以无法编写使用数组类型形参的函数,而且通常需要通过操纵指向数组中的元素的指针来处理数组。

    //数组形参的定义 void A(int*); void A(int []); void A(int [10]); //三者等价,形参类型都为int* //通常将数组形参直接定义为指针要比使用数组语法定义更好

    形参的长度会引起误解

    编译器会忽略任何数组形参指定的长度,当编译器检查数组形参关联的实参时,它只会检查实参是不是指针、指针的类型和数组元素的类型是否匹配,而不会检查数组的长度。所以调用时要防止数组越界。

    数组实参

    在传递数组时,实参是指向数组第一个元素的指针,形参复制的是这个指针的值,而不是数组元素本身,因此并不会修改实参指针的值。然而,函数可以通过该指针改变它所指向的数组元素的值。通过指针形参做的任何改变都在修改数组元素本身。

    不需要修改数组的值时应将形参定义为指向const对象的指针。

    通过引用传递数组

    形参是数组的引用,编译器不会将数组实参转化为指针,而是传递数组的引用本身。此时编译器会检查数组实参的大小与形参的大小是否匹配:

    void test(int (&arr)[10]) {/*...*/} int main() { int i = 0,j[2] = {0,1}; int k[10] = {0,1,2,3,4,5,6,7,8,9}; test(&i); //error test(j); //error test(k); //ok,test严格接受含有10个int型数值的数组 } //&arr两边的圆括号是必需的,因为下标操作具有更高的优先级: f(int &arr[10]); //error, arr表示引用的数组,数组元素为引用类型 f(int (&arr)[10]); //ok,arr是含有10个int型元素数组的引用

    多维数组的传递

    void test(int matrix[][10], int rowSize); //形参是一个指针,指向数组的数组中的元素

    传递给函数的数组的三种处理:

    数组本身放置一个标记来检测数组的结束。例如:C风格字符串以空字符null作为结束标记

    传递指向数组第一个和最后一个元素的下一个位置的指针

    void test(const int *beg, const int *end) { while (beg != end) cout<< *beg++ <<endl; } int main() { int j[2] = {0,1}; test(j,j+2); //j转化为指向j数组中第一个元素的指针,j+2指向最后一个元素的下一个 return 0; }

    显式传递表示数组大小的形参,即将第二个形参定义为表示数组的大小

    void test(const int ia[], size_t size) { for (size_t i = 0; i != size; ++i) cout << ia[i] <<endl; } int main() { int j[2] = {0,1}; test(j, sizeof(j)/sizeof(*j)); //小技巧:sizeof(j)/sizeof(*j)可表示数组的长度 return 0; }

    main:处理命令行选项

    //假设主函数main位于名为prog的可执行文件中,可如下将实参传递给程序: prog -d -o ofile data0 //实际上main定义了两个形参: int main(int argc, char *argv[]) //第二个形参argv是一个C风格字符串数组 第一个形参argc则用于传递该数组中字符串的个数 //也可以这样定义 int main(int argc, char **argv) //argv中第一个字符串通常为程序的名字,接下来将额外可选的字符串传递给主函数main,则上述命令行表示: argc = 5; argv[0] = "prog"; argv[1] = "-d"; argv[2] = "-o"; argv[3] = "ofile"; argv[4] = "data0";

    return语句

    两种形式:

    ​ return;

    ​ return expression;

    没有返回值的函数

    只能用于返回类型为void的函数,且return语句不是必需的,隐式的return发生在函数的最后一句完成时。返回类型为void的函数通常不能使用第二种形式的return语句,但是它可以返回另一个返回类型也是void的函数的调用结果:

    void do_swap(int &v1, int &v2){ int tmp = v2; v2 = v1; v1 = tmp; //ok,可以不显示使用return语句 } void swap(int &v1, int &v2){ if(v1 == v2) return false; //error,void function cannot return a value return do_swap(v1,v2); //ok,returns call to a void function }

    具有返回值的函数

    任何返回类型不是void的函数都必须返回一个值,而且这个返回值的类型必须和函数的返回类型相同,或者能隐式转化为函数的返回类型;

    在含有return语句的循环后没有提供return语句是很危险的,因为大部分的编译器不能检测出这个漏洞,运行时会出现什么问题是不确定的。

    主函数main的返回值

    允许主函数main没有返回值就可结束,编译器会隐式插入一个返回0的语句

    返回非引用类型

    函数的返回值用于初始化在调用函数处创建的临时对象,当返回非引用类型时,其返回值既可以局部对象,也可以是求解表达式的结果,在调用函数的地方会将函数返回值复制给临时对象。

    返回引用

    没有复制返回值,返回的是对象本身

    千万不要返回局部对象的引用

    const string &manip(const string& s){ string ret = s; return ret; //error,当函数执行完毕,字符串ret占用的储存空间被释放,函数返回值指向了对于这个程序来说不再有效的内存空间 } //应将ret定义在函数外部

    引用返回左值

    返回引用的函数返回一个左值

    char &get_val(string &str, string::size_type ix){ return str[ix]; } int main() { string s("a value"); cout<<s<<endl; get_val(s,0) = 'A'; //允许给函数返回值赋值 cout<<s<<endl; return 0; }

    千万不要返回指向局部对象的指针

    和返回局部引用一样,函数结束,局部对象被释放,指向它的指针变成了野指针(悬垂指针)

    递归

    直接或间接调用自己的函数;

    递归函数必须定义一个终止条件,否则无限递归导致调用栈耗尽;

    主函数不能调用自身

    函数声明

    如变量必须先声明后使用一样,函数也是;

    **在头文件中提供函数声明;**定义函数的源文件应包含声明该函数的头文件;

    默认实参

    调用包含默认实参的函数,可以为该形参提供实参,也可以不提供。如果提供了实参,则它将覆盖默认的实参值,否则,函数将使用默认实参值;

    通常,应在函数声明中指定默认参数,并将该声明放在合适的头文件中

    局部对象

    C++中,每个名字都有作用域,而每个对象都有生命周期,就是在程序执行过程中对象存在的时间

    自动对象

    只有当定义它的函数被调用时才存在的对象;

    自动对象在每次调用函数时创建和撤销;

    形参也是自动对象;

    静态局部对象

    static局部对象确保不迟于在程序执行流程第一次经过该对象的定义语句时进行初始化。一旦被创建,在程序结束前都不会被撤销

    size_t count_calls(){ static size_t ctr = 0; //ctr一直存在 return ++ctr; } int main() { for (size_t i = 0; i != 10; ++i) cout<<count_calls()<<endl; return 0; }

    内联函数

    内联函数避免函数调用的开销

    把内联函数放入头文件中定义,保证在调用点该函数的定义对编译器可见,这一点不同于其他函数

    注意:在头文件中加入或修改内联函数时,使用了该头文件的所有源文件都必须重新编译

    类的成员函数

    编译器隐式地将在类内定义的成员函数当作内联函数;

    this指针:每个成员函数(除了static成员函数)都有一个额外的、隐含的形参this。在调用成员函数时,形参this初始化为调用函数的对象的地址;

    this指向const对象,const成员函数不能修改调用该函数的对象;

    const对象、指向const对象的指针或引用只能用于调用其const成员函数;

    由于this指针是隐式定义的,因此不需要在函数的形参表中包含this指针,实际上,这样做也是非法的;

    构造函数:

    ​ 如果没有为一个类显式定义任何构造函数,编译器将自动为这个类生成默认构造函数;

    ​ 合成的默认构造函数一般适用于仅包含类类型成员的类。而对于含有内置类型或复合类型成员的类,则通常应该定义它们自己的默认构造函数初始化

    重载函数

    出现在相同作用域中的两个函数,如果具有相同的名字而形参表不同,包括形参类型和个数不同则称为重载函数

    函数重载和重复声明的区别

    两个函数声明的返回类型和形参表完全匹配,则第二个函数声明视为第一个的重复声明

    //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 Record lookup(const Phone&, const Name&); //default argument doesnot change the number of parameters Record lookup(const Phone&, const Name& = " "); //const is irrelevent for nonreference parameters Record lookup(Phone); Record lookup(const Phone); //redeclaration

    待补充

    指向函数的指针

    用typedef简化函数指针的定义

    typedef bool (*cmpFcn)(const string &, const string &); //定义cmpFcn是一种指向函数的指针类型的名字。该指针类型为“指向返回bool类型并带有两个const string引 //用形参的函数的指针”,要使用这种函数指针类型时,只需直接使用cmpFcn即可

    指向函数的指针的初始化和赋值

    在引用函数名但又没有调用该函数时,函数名将自动解释为指向函数的指针;

    函数指针只能通过同类型的函数或函数指针或0值常量表达式进行初始化或赋值

    通过指针调用函数

    可以不使用解引用操作符,直接通过指针调用函数:

    cmpFcn pf = lengthCompare; lengthCompare("hi","bye"); //direct call pf("hi","bye"); //也可 (*pf)("hi","bye"); //也可

    返回指向函数的指针

    //写法 int (*ff(int)) (int*, int); //由里往外理解 //使用typedef可使该定义更简洁 typedeff int (*PF)(int*, int); PF ff(int); //ff返回一个函数指针

    指向重载函数的指针

    void ff(vector<double>); void ff(unsigned int); void (*pf1)(unsigned int) = &ff; //与ff(unsigned)匹配 //必须与重载函数的一个版本精确匹配,否则报错
    Processed: 0.010, SQL: 9