C++ Primer 总结之Chap7 Functions

本系列为本人温习C++基础时所记的tips,欢迎各位同学指正,共同进步TvT。

我写的总结是摘抄英文加上一些自己的理解,很多地方用英文是因为翻译了实在变味,不如英文原文写得精准。耐心看下去,会有收获的TvT

已经好久没更新了,这一篇其实是几个月前就整理好的,拖延症晚期… 接下来会加快速度了!Fighting!

void fun(); void fun(void); // equivalent
  1. 使用指针而不用引用的情况:

    • 赋值可能为空,则不可用引用,因为引用必须确保绑定到一个对象上去
    • 非必须但建议在传参且需要修改到实参的时候用指针来传
  2. 形参(parameter)和实参(argument)要分开来;调用函数的时候,先用实参来初始化形参,再进入函数体。而初始化形参的前后顺序是不规定的,所以应避免实参的表达式具有副作用,如fun(i++, i+1),或者是会互相影响的函数fun(g1(), g2())

  3. 函数返回类型不能直接是函数或数组,但是可以是指向该函数或数组的指针。

  4. 传参的时候是按值还是按引用,主要是看形参的声明里有没有带&,不看实参。总而言之就是按正常初始化的逻辑来。传的如果是指针的值的话,实参指针本身不会因为形参的改变而改变,但是指向的值是能够被影响的,想防止的话就加上个const。此种情况为pointer to const,但是一般可以用const reference来替代,除非pointer有可能为空。

  5. 直接复制实参到形参的限制:

    • 无法改变实参的值;
    • 复制的时候必然会导致时间和空间上的损耗(class类型应默认使用const reference,除非该对象拷贝的代价小到可以忽略
    • 对象可能无法被复制
  6. 用到reference的时候一般都会用const reference,因为从调用方的角度是看不出来是否会修改实参的。如果函数确实要修改实参,一般建议用指针来传参。

  7. 当有多于一个结果要return时,可以将多余的变量以函数的按引用传值参数的形式进行返回。

  8. plain reference比起const reference来有很多限制,比如:

    • 不能用literal const来初始化;
    • 不能用const值初始化;
    • 必须用同类型的rvalue来初始化,连隐式类型转换都不允许。

    所以,当read-only,或者目的仅是防止copy来提高效率的时候,都要用const reference。

    void fun1(const int& a){}
    void fun2(int& a){}
    short a = 1;
    fun1(a); // ok
    fun2(a); // error, disallow implicit cast
    
    
    // Passing a reference to a pointer
    // Swap the pointers, NOT the values pointed by pointers
    void ptrswap(int* &p1, int* &p2){
        int *temp = p1;
        p1 = p2;
        p2 = temp;
    }
    
    
    // Use a reference parameter to get additional infos
    // cnt records the number of this value, ret is the iterator pointing to the first value found and end if no one exists
    vector<int>::const_iterator findVal(vector<int>::const_iterator begin, 
    vector<int>::const_iterator end, vector<int>::size_type val, int& cnt){
        vector<int>::const_iterator ret = end;
        for(; begin < end; ++begin){
    	if(*begin == val){
    		++cnt;
    		if(ret == end)
    			ret = begin;
    	}
        }
        return ret;
    }
    
  9. vector这种容器可以整个地作为参数传递进去,但是要考虑效率。所以一般传的都是iterator.

  10. 函数参数列表中出现的数组就是指针,但有时候区分写法是有意义的,写成数组可以明确表明指向的是多个对象(的首地址)。

  11. Passing an Array by Reference

    void printValues(int (&arr)[10] ){
        for(size_t i = 0; i != 10; ++i){
            cout << arr[i] << endl;
        }
    }
    

    必须是int (&arr)[10]而非int &arr[10],前者是有10个int元素的数组的引用,后者是10int引用元素的数组(一般不存在,或者说不允许),因为[]的优先级比&高。
    另外,这里也必须表明实际数组大小!改动会影响原数组

  12. 传递多维数组

// 多维数组,括号同必需
void printVals(int (*matrix)[10], int rowSize); // or
void printVals(int matrix[][10], int rowSize);

// e.g
void func(int (*arr)[3]){
        arr[0][0] = 5;
}
int a[2][3] = {{1},{2}};
func(a);
cout << a[0][0]; // 5

  1. int main(int argc, char *argv[]),第一个参数表示第二个参数的size大小,第二个参数表示main函数运行时输入的参数字符串们,一般第一个是程序名。

  2. void函数可以直接return也可以返回一个返回类型同为void的函数的调用结果。

  3. return值时是复制,引用时是直接返回其本身。但注意不能返回函数内局部变量的引用,因为调用完该函数这个局部变量就挂掉了,返回它的引用也没用。

    const string &shorter(const string &s1, const string &s2){
        string s;
        return s1 < s2 ? s1 : s2;
        return s; // error, local object
    }
    
  4. 返回non-reference的情况一般只出现在操作符重载,或返回*this实现链式调用中。

  5. Recursion

    int gcd(int v1, int v2){
        if(v2==0)
            return v1;
        return gcd(v2, v1%v2);
    }
    

Function Declaration

  1. 函数的声明定义跟变量差不多,都是只能定义一次但可以在多处声明。The declaration is also called function prototype. It’s the interface between the programmer who defines the function and programmer who uses it.

  2. Three elements of function declaration: function name, parameter list, return type

  3. Function declarations should be put into the header and defined in one source file which includes the header. This ensures that all the declarations for a given function agree(consistency). If the interface changes, only one declaration must be changed.

  4. 无论函数的定义在哪个源文件中实现都没所谓的,只要你在使用的地方前面写了该函数的声明就可以。而一般我们是用包含头文件的方式来使用该函数的(包含后就声明了,声明了就能用了)。定义该函数的源文件也最好包含该头文件,虽然其实可以不包含的。还有一点要注意的是,如果包含之后定义的函数的参数与声明不同而名字相同,会被视作重载而非报错。如果是返回类型不同而参数列表和函数名相同,则会报错。一定程度上提高安全性。

  5. 使用默认参数的时候不可引发歧义,比如

    void fun(int);
    void fun(int, int = 5);
    
    fun(3); // error, confusion here
    

    The parameters behind a default parameter should also be default. The default value can be an expression. However, we should take care of the cost and use biult-in types if it’s possible.

    【注意】一般将默认参数放在函数声明里头。因为若将其放在定义中,则只有在定义该函数的文件中才能用默认参数,其他包含了声明的头文件的源文件并不知道默认参数的存在。若声明和定义的默认参数不同,那么简单说来就是只包含定义的文件以定义为准,只包含声明的文件以声明为准,同时包含定义和声明的文件会出现默认参数重定向错误。**这也就是我们建议定义函数的文件同时包含声明函数的头文件的原因——一定程度上提高一致性,减少模糊/歧义。

  6. 函数中局部静态变量的初始化发生在其定义时且只初始化一次,而并非在用到函数的时候就将其中的所有static变量都初始化(比如有些在if语句中没有执行到的就不会初始化),但是,我们建议把函数中static变量的定义放在函数开头的位置,以避免违反“直觉”;在Java中,根本就不允许static放在函数里边,所以也就不存在这个问题了,类被加载的时候就会把其包含的全部static初始化了。

Inline Function

  1. 内联函数是提高性能的重要一步!在返回类型前边加上inline,可以请求编译器将该函数的调用替换为函数体以提高效率。至于编译器鸟不鸟你嘛,那就得看脸了。不过一般来说,递归函数还有太TM长的函数是不可能会被同意的,一般是短小精悍的可以被内联。

  2. Unlike other function definition, inline functions should be defined in header files.

    因为如果你要将内联函数expand出来的话(在编译时,link前发生),仅有函数prototype是不够的。

    很容易想到的问题是函数多次定义。所以C++规定,只要在前面都加上inline就不算重定义,每个源文件都可以有这个inline函数的定义(而且其实这些定义居然还可以不一样 )。实际上,这些定义仅对定义该内联函数的源文件本身可见,不能像其他函数一样可以被其他源文件声明之后使用。不过,想来这样干也还不知道有啥用,一般是将内联函数定义在头文件里然后包含到需要使用的源文件里头去的。按照上面的理解,各自不可见,相安无事。(只是自己的理解而已,需要再看资料)

    【注意】当头文件中添加或者修改内联函数之后,所有包含该头文件的源文件都需要重新编译。

Class Member Function

  1. 类的成员函数可以在类的定义里边实现,也可以在类外边实现。如果在里边实现的话,默认为内联函数。

  2. 【注意】成员函数可以访问所有同类对象的private成员。因为成员函数是类的成员函数,而并非某个特定对象的成员函数!!!

  3. 成员函数有一个不可见的隐式参数,即函数的调用者。当函数调用的时候,调用者本身也被作为一个参数传入到函数中去啦。这个隐式参数其实就是this,在non-const non-static成员函数中,类型是T* const,表示该指针的内容不可变但其指向的对象是可变的;而在const non-static成员函数中,类型是const T* const,即二者均不可变。

  4. 当成员函数是const的时候,其实是将this的类型在这里改为 const T* const。此时无法修改类的其他成员。

  5. const对象或者指向const对象的指针只能调用const函数,不能调用non-const函数,因为它们可能会修改其成员,而这是const对象不允许的。

struct Node{
    Node *next;
    void appendTo(Node *node){
        next = node->next;
        node->next = this;
    }
};
  1. member function的定义可以放在类的外面,但是要写明它是哪个类的,用scope operator::来。

  2. 默认构造函数(Default Constructor)是指 constructor that takes no arguments, NOT parameters,只要形参都有默认参数,则在创建对象的时候照样可以啥实参都不提供直接使用默认构造函数。一个类不能有多个默认构造函数。

  3. Constructor Initialization List

    // 类的成员中包含类的,作为成员的对象对应的类必须有默认构造函数或在初始化列表中显示地构造
    class B{
    public:
        B(int a = 5){cout << a;} 
        // If remove default value, then you
        // should explicitly call B(someVal)
        // at the constructor initialization list of A, (in this case, output someVal and 4)
        // or the compiler will complain because some
        // error happen when it implicitly 
        // call B() to find it not possible
        int a;
    };
    
    class A{
    public:
        A():a(0), b(0.0){
            temp = B(4);
        }
    private:
        int a;
        double b;
        string c; // No need to initialized to ""
        // in the constructor init list
        B temp;
    };
    
    int main(){
        A a;
    
        return 0;
    }
    // Output: 54
    
    

    :{左侧的部分就叫做初始化列表了,应该尽量多地使用它。

    【注意】类成员中,内置类型不显式地初始化那么其值会是“未定义的”!!(【除非】这个类的对象是个全局变量或者local static object,此种情况会被初始化为0)因此每添加一个数据成员,则须在每个构造函数中都将其初始化,否则将会导致很难发现的bug; 而针对class type则会在初始化列表处隐式地调用其默认构造函数( mClass() ),那么显然,在这种情况下如果mClass不存在默认构造函数的话会出事的。

    当然,如果你显式地在此处调用它的构造函数,那它当然不会隐式调用多此一举啦。

    class B{
    public:
        int fuck;
        B(){cout << fuck;}
    };
    
    class A{
    private:
        int a;
        B temp;
    public:
        A(){
    	    cout << a << endl;
        }
    
    };
    
    A haha;
    
    int main(){
    
        A hehe;
        
        // Output: 00 
        // -858993460-858993460
        // hehe.a uninitialized, hehe.temp call default function,
        // hehe.temp.fuck also uninitialized
        
        return 0;
    }
    
  4. 当不存在任何构造函数时,会自动给你生成一个默认的构造函数,对成员中的类调用其默认构造函数,内置类型则看情况,全局/static则初始化为0(如上里所示,包括其成员temp的内置类型成员也是这个规则),否则不初始化,其值不可知。So, classes with built-in or compound type should usually define their own default constructors to initialize the members.

  5. C++并不强制规定class名字与源文件名相同,与Java不一样。但通行的做法是这样子的,所以最好还是这样。

Overloaded Functions

  1. 重载是为了 define a set of functions that have similar general action but apply to different parameter types. 要注意重载的目的是什么,而不是瞎几把乱用。 而且有时候也要考虑,是不是全部用一个名字反而会造成使用上的不便(lose information that was inherent in the function names and make the program more obscure)?举个栗子:

    // Cursor moving
    Screen& moveHome();
    Screen& moveAbs(int, int);
    Screen& moveRelat(int, int, char *direction);
    
    
    // Compared to
    Screen& move();
    Screen& move(int, int);
    Screen& move(int, int, char *direction);
    // 这样反而,尴尬了
    
  2. 操作符重载也是重载的一种。main函数有且只有一个,不存在被重载的可能。

  3. 编译器根据参数来判断调用哪个重载函数中的哪个,不关返回类型的事儿。所以啊,如果你让编译器搞不懂你的意图,那就出事了,特别是在配合默认参数的时候。

  4. string init();
    void fun(){
        int init = 0;
        string s = init(); // error
    }
    
  5. In C++ name lookup happens before type checking.

    先找到名字对应的函数,然后再检查类型是否符合。

    // Name hiding rather than overloading
    void print(const string& str){cout << str;}
    void print(int i){cout << i;}
    void fooBar(int val){
        void print(int); // hides previous print
        print("hello"); // error
        print(3); // ok
    }
    
    
  6. Function matching in overloading:3 possible outcomes

    • find a best match function, 【注意】 如果找到的best match只是声明而没有定义的话,它也不会退而求其次去用那些已经定义好的次一点的能match的函数。 可以将一个函数名看做一个目录,下面有多个行数,一行代表一个重载的版本,最先匹配到哪个就要用哪个。
    • no function matches
    • ambiguous
  7. Distinguish overloading from redeclaring a function:

    void fun(int);
    void fun(int a);
    void fun(int a = 1);
    void fun(const int a);
    // All above are redeclaring functions
    
    // However, for const, reference and pointer are different
    
    
    // These are different
    void fun(int &a);
    void fun(const int &a);
    
  8. Redeclaring is sometimes helpful when we want to do some extra work in our scope:

    • Explicitly say that you want to use which overloaded function instead of decided by compiler, actually this is name hiding!
    • Giving default value or adding const restriction(to nonreference/pointer parameters)
    void fun(char c){cout << c << endl;}
    void fun(short){cout << "short" << endl;}
    void fun(int){cout << "int" << endl;}
    
    int main{
        char c = 'c'; // match int if no char
        fun(c); // output 'c'
        void fun(short);
        fun(c); // output "short"
        void fun(char c = 'd');
        fun(); // output 'd'
        
        
        return 0;
    }
    

    For reference/pointer, const differs them, it’s not redeclaring but overloading, because compiler can distingusish them from each other.

    However, for non-reference parameter, const or not are the same. Because the parameter is passed as a copy!!! It has no effect on the arguments that are past to the function. Also for pointers which are const themselves.

    void fun(int &p){cout << 1;}
    void fun(const int &p){cout << 2;}
    int main(){
        int a;
        const int b = 0;
        fun(a); // Both are viable, however,
        // the match from const reference to a non-const 
        // reference needs conversion, so there's no confusion; output: 1
        fun(b); fun(3); // Only the const version matches, output: 2
    }
    
  9. Three steps for overloading resolution

    • Find the functions with the same name
    • Select functions that are viable, or say, can be called if there is only one of them exists
    • Find a best match guy. To be best, there should be at least one parameter better than the other functions, and no worse than the other functions for the remaining parameters. If it’s ambiguous, then it’s an error.
    void fun(int, int);
    void fun(double, double);
    
    int main(){
        fun(3, 3.0); // error
        
        
        return 0;
    }
    
  10. The match precedence:

    1. Exact match.
    2. Match through a promotion
    3. Match through a standard conversion
    4. Match through a class-type conversion
    void fun(long);
    void fun(float);
    
    int main(){
        fun(3.0); // Ambiguous, 3.0 is considered as double,
        // and a standard conversion is never better than another
        
        return 0;
    }
    
    • An integral value that happens to have the same value as an enumerator type cannot be used to call a function expecting an enum argument. The reverse direction is okay.
  11. char *p = "hello";为何不报错?——首先,"hello"的类型是const char [6],只是它能够被转化为cosnt char*而已。允许这个是为了兼容以前的程序,在字符串这一块儿做特殊处理。如果是const char *pp = "pp"; p = pp;的话就会报错啦。另外,认真按理说,用"hello"凭什么初始化一个指向char的指针?就是因为以前C的时候就是按这个逻辑来特殊处理字符串的。还有输出这一块也做了重载,cout << p;不是输出地址而是输出字符串,如果要强行输出地址可以将其转换为void*或者使用printf("%p");

  12. 类的成员函数根据其是否const可以算重载。因为其实参数列表后边那个const是修饰一个隐藏的参数this的,而之前说过了,指针加不加const是可以分辨的,也就是说可以根据调用该函数的对象(this)是不是const来决定调用哪个函数。

  13. When will copy constructor being called:

    • Use an object to initialize
    • Use non-reference class as parameter of function
    • Return a non-reference class object

Pointer to Function

// pf is a pointer to function, '()' is necessary
bool (*pf) (const string&, const string&);

// pf2 is a function returning bool*
bool *pf2 (const string&, const string&);

// define a new type called cmpFcn
typedef bool (*cmpFcn) (const string&, const string&);

bool compare(const string&, const string&);
cmpFcn pf3 = compare;
cmpFcn pf4 = &compare;
pf3("a", "b");
(*pf3)("a","b");


// Pointer to function as parameter, equivalent
void useBigger( bool(const string&, const string&) );
void useBiggger( bool (*) (cosnt string&, csont string&) );


// returning a pointer to function
typedef int (*PF) (int*, int); // use ff(int) to 
// replace PF, means the return type of ff(int) 
// is a pointer to function

// ff is a function taking an int and returning a
// function pointer which points to a function
// taking int* and int and returning int
int (*ff(int)) (int*, int);
PF ff(int); // equivalent, Pf is a return type
  1. A function’s type is determined by its returned type and parameter list. A function’s name is not part of its type.

  2. When we use a function name without calling it, the name is automatically treated as a pointer to a function. Applying the address-of operator to the function name has the same effect of the function name itself.

Reference : C++ Primer 4th edition(评注版)

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值