我的C++primer长征之路:函数

函数

局部对象

局部变量会隐藏外层作用域中同名的其他所有声明。
局部静态对象
在程序的执行路径第一次经过对象定义语句时初始化,并直到程序终止才会被销毁,即使对象所在的函数结束执行也不会对它有影响。
如果局部静态变量没有显式的初始化,它将会被进行值初始化,内置类型的局部静态变量初始化为0.

参数传递

传值参数

初始化一个非引用类型的变量时,初始值被拷贝给变量,因此对变量的改动不会影响初始值。

指针形参

传递指针形参时,拷贝的是指针的值,也就是地址。这是两个不同的指针。通过指针可以间接访问所指向的对象并修改对象的值。

int n = 0, i = 2;
int *p = &n, *q = &i;
*p = 2; //n的值变为2
p = q; //p指向了i
void reset(int *ip){
    *ip = 0;//改变了ip所指向的对象的值,也就是i的值
    ip = 0; //只改变了ip的局部拷贝,实参未被改变
}

int i = 42;
reset(&i);
cout << i << endl; //输出结果是i=0.

传引用参数

使用引用传递参数可以避免拷贝,减小计算消耗。此外,还有一些类类型不支持拷贝操作(IO类型等),只能通过引用形参访问该类型对象。

bool isShorter(const string &s1, const string &s2){ //若无需改变引用形参的值,最好将其声明为常量引用
    return s1.size() < s2.size();
}

const形参和实参

顶层const作用于对象本身。底层const作用于对象(通常指针)所指向的对象。
与其他初始化一样,用实参初始化形参时,会忽略掉顶层const。也就是说形参有顶层const时,传给他常量对象或者非常量对象都是可以的。
忽略顶层const可能会产生一些意想不到的问题。

void func(const int i){...} //func能读取i,但不能写
void func(int i){...} //错误,重复定义了func,因为顶层const被忽略了。

数组形参

数组的特殊性质:

扫描二维码关注公众号,回复: 11316709 查看本文章
  • 不允许拷贝数组
  • 使用数组会将其转换成指针
    所以向一个函数传递数组时,其实传递的是数组首元素的指针。
//三种等价形式,其形参都是const int *
void print(const int *); //
void print(const int[]); //
void print(const int[10]); //这里的维度表示的是我们希望数组有多少维度,实际不一定

数组引用形参

void print(int (&arr)[10]){
    for(auto elem : arr){
        cout << elem << endl;
    }
}

int &arr[10]; //这是一个包含10个引用的数组
int (&arr)[10]; //这是一个指向包含10个整数的整型数组的引用

main处理命令行参数

int main(int argc, char *agrv[]){...} //argv是一个数组,其中的每个元素是指向一个C风格字符串的指针, argc表示数组中字符串的数量

//等价于
int main(int argc, char **agrv){...}

argv[0]是程序的名字,可选的实参从argv[1]开始。

含有可变形参的函数

  • 如果所有形参类型相同,可以传递一个名为initializer_list的标准库类型。
  • 如果实参类型不一样,可以编写可变参数模板来实现可变形参的函数。
  • 特殊形参类型,即省略符。一般只用于与C函数的交互接口程序。

initializer_list

initializer_list定义在同名头文件中。

initializer_list提供的操作
initializer_list lst; 默认初始化T类型的空列表
initializer_lsit lst{a, b, c, …}; lst的元素和初始化列表一样多,lst元素是对应初始值的拷贝,列表中的元素是const.
lst2(lst); lst2 = lst 拷贝或者赋值一个initializer_list对象不会拷贝列表中的元素;拷贝后原始列表和副本共享元素
lst.size(); lst中的元素数量
lst.begin(); 返回指向lst的首元素指针
lst.end(); 返回指向lst尾元素后一个位置的指针。
void error_msg(initializer_list<string> il){
    for (auto beg = il.begin(); beg != il.end(); ++beg){
        cout << *beg << endl;
    }
}

string expect = "hhh";
string actual;
cin >> actual;
if(expect != actual){
    //如果向initializer_list形参中传递一个值的序列,必须把序列放在一个花括号内。
    error_msg({"functionX", expect, actual});
}
else
{
    error_msg({"functionX", "ok"});
}

当然,含有initializer_list的函数还可以拥有其他形参。

省略符形参

省略符形参会调用varargs的C标准库功能。…只能用于C和C++都通用的类型,对于大多数类类型的对象在传递给省略符形参时都无法正确拷贝。
省略符形参只能出现在形参列表的最后位置。

void f(param_list, ...);

不要返回局部对象的引用或者指针。

因为函数返回后,局部对象(静态局部变量除外)的存储空间就会被释放掉,也就是说局部变量的引用将指向无效的地址。

返回类类型的函数和调用运算符

如果函数返回的是指针、引用或者类的对象,那么可以使用函数调用的结果来访问对象的成员。

//调用返回的string对象的size成员
auto sz = shorterString(s1, s2).size();

列表初始化返回值

C++11新标准中,函数可以返回花括号包围的值的列表。
可以像这样:

vector<string> process(){
    //expected, actual是string对象
    if(expect.empty()){
        return {};
    }
    else if (expect == actual){
        return {"functionX", "ok"};
    }
    else{
        return {"functionX", expect, actual};
    }
}

如果函数返回的是内置类型,则花括号包围的列表最多包含一个值。

返回数组指针

返回函数指针主要有四种方法:一般的声明方式、使用类型别名、使用尾置返回类型以及使用decltype。

使用类型别名

typedef int arrT[10]; //arrT是类型别名,表示的是含有10个整数的数组
using arrT = int [10]; //等价形式
arrT* func(int i); //返回指向含有10个整数的数组的指针。

一般的声明方式

如果要定义返回数组指针的函数,数组的维度必须跟在函数名字之后,且放在形参列表之后。
type(*function(parameter_list))[dimension]

int (*func(int i))[10]; //返回指向包含10个int元素的数组的指针
int *func(int i)[10]; //没有外层括号,返回的则是包含10个int指针的数组

使用尾置返回类型

使用尾置返回类型可以简化上述声明。

//表明函数接收一个int实参,返回一个指针,该指针指向含有10个整数的的数组
auto func(int i)->int (*)[10];

使用decltype

若直到函数返回的指针指向哪个数组,就可以使用decltype声明返回类型。

int odd[] = {1, 3, 5};
int even[] = {2, 4, 6};
decltype(odd) *arrPtr(int i){ //decltype不负责把数组类型转化成对应指针,所以arrPtr前面要加上*
    return (i % 2) ? &even : &odd;
}

小练习:编写一个函数声明,使其返回数组的引用,该数组包含10个string对象。

string (&func(int i))[10];
//使用类型别名
using s10 = string [10]//等价于typedef string s10[10];
s10 &func(int i);
//尾置返回类型
auto func(int i)->string (&)[10];//这样理解,首先是一个引用&,这个引用绑定到string[10]数组中。
//s使用decltype
string s[10];
decltype(s) &func(int i);

main函数的返回值

main函数允许没有return语句直接结束,编译器会隐式地插入一条返回0的语句。
函数递归调用消耗栈空间。main函数不能调用自己。

函数重载

main函数不能重载。
一个作用域内,函数名字相同,形参列表、返回值不同则是函数重载。

const形参的问题

顶层const不影响传入函数的对象,也就是说无法将含有顶层const的形参和不含有顶层const的形参区分开来。

int func(int i);
int func(const int i);// 重复声明了func

int ptr(int* p);
int ptr(int* const p); //重复声明了ptr

对于接收引用和指针的函数来说,对象是常量还是非常量,对应的形参不同

int fun(int *p);//函数作用于指向int的指针
int fun(const int *p); //新函数,作用域指向常量的指针

int ref(int &i); //函数作用于int引用
int ref(const int &i);//新函数,作用于常量引用

当传递一个非常量对象或者指向非常量对象的指针时,会优先调用非常量版本函数。

const_cast和重载

const string &shorterString(const string& s1, const string& s2){
    return s1.size() < s2.size() ? s1 : s2;
}

参数和返回类型都是const string&。函数传入两个非常量string后,得到的是const string的引用,这样一来,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);
}

这样一来,传入2个非常量string,比较得到较短的对象的非常量引用,这样显然是安全的。

在C++中,名字查找发生在类型检查之前。

默认实参

typedef string::size_type sz;
string screen(sz ht = 24, sz wid = 80, char backgrnd = ' ');

一旦某个形参被赋予了默认值,后面的所有形参必须有默认值。

默认实参声明
在给定作用域中,一个形参只能被赋予一次默认实参。

//表示高度和宽度没有默认实参
string screen(sz, sz, char = ' ');
//不能修改一个已经存在的默认值
string screen(sz, sz, char = '*');//错误,重复声明了
//但是可以像下面这样添加默认实参,因为第三个参数已经有了默认实参,所以符合之前的标准。
string screen(sz = 24, sz = 40, 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, '*')
}

内联函数

内联函数可以避免函数调用的开销。

inline const string & shorterString(const string & s1, const string &s2){
    return s1.size() < s2.size() ? s1 : s2;
}

constexpr函数

函数的返回类型和所有形参类型都是字面值类型,函数体中必须有且只有一条return语句。

constexpr int new_sz(){return 42;}
constexpr int foo = new_sz(); //正确,foo是一个常量表达式

编译器在编译时能够验证函数的返回值类型。

constexpr函数的返回值不一定是一个常量。

//如果arg是常量表达式,则scale(arg)也是常量表达式,反之则不然
constexpr size_t scale(size_t cnt){
    return new_sz() * cnt;
}
int arr[scale(2)]; //正确,scale(2)是常量表达式
int i = 2;
int arr2[scale(i)]; //错误,scale(i)不是常量表达式

一般情况下,把内联函数和constexpr函数放在头文件中。

调试帮助

assert预处理宏,定义在cassert头文件中。

string s("hello");
assert(s.size() > 5);

assert的行为依赖于NDEBUG预处理变量的状态。如果定义了NDEBUG,则assert什么也不做,默认状态下没有定义NDEBUG,assert将执行运行时的检查。

CC -D NDEBUG main.c

等价于在main.c文件开始写#define NDEBUG。

void print(const int a[], size_t size){
    #ifndef NDEBUG
    //__func__是编译器定义的一个局部静态变量,用于存放函数名字,是一个const char的静态数组。
    cerr << __func__ << endl;
    #endif
    ...
}
  • __ FILE __ 存放文件名的字符串字面值
  • __ LINE __ 存放当前行号的整型字面值
  • __ TIME __ 存放文件编译时间的字符串字面值
  • __ DATE __ 存放文件编译日期的字符串字面值

函数匹配

const实参

如果重载函数的区别是形参的引用类型是否引用了const,或者指针的形参是否指向了const,则调用时编译器通过实参是否是常量来决定使用哪个函数,如下:

int f(int &i);
int f(const int &i);
const int a = 0;
int b = 1;
f(a); //调用f(const int &i),不能把普通引用绑定到const对象上。
f(b); //调用f(int &i)

函数指针

  • 函数的类型由其返回值类型和形参类型共同决定。
  • 函数是不能直接返回函数的,也不能直接返回数组。(只能返回函数指针或数组指针。)
bool shorterString(const string&, const string&);

该函数类型是bool (const string&, const string&)。声明一个函数指针,则只需要将函数名替换成指针即可。

//未初始化的指针
bool (*pf)(const string&, const string&) //指针的括号不能省,如果省略,pf就是一个返回值为bool*的函数

把函数名作为一个值使用时,跟数组一样,都会自动地转换成指针。

pf = shorterString;
pf = &shorterString; //两种形式等价

函数指针可以调用该函数,无需提前解引用指针。

bool b1 = pf("hello", "wolrd!");
bool b2 = (*pf)("hello", "world!");//等价形式
bool b3 = shorterString("hello", "world!");

指向不同函数类型的指针不存在转换规则。也就是说函数指针指向的函数的类型必须与指针类型一致。

重载函数的指针

指针类型必须与重载的函数中的某一个类型精确匹配。

函数指针形参

与数组类似,不能直接定义函数类型的形参,但是可以定义指向函数的指针类型的形参。

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 &));

//还可以直接把函数当作实参使用,此时函数会被转换成函数指针。
useBigger(s1, s2, shorterString);

返回指向函数的指针

和数组类似,虽然不能直接返回函数,但可以返回指向函数的指针。
但是编译器不会自动将函数返回类型当成对应的指针类型处理。简便方式就是使用类型别名来声明返回函数指针的函数。

using F = int(int*, int); //F是函数类型
using PF = int(*)(int*, int); //PF是指针类型
PF f(int); //正确,PF是指向函数的指针,f1返回函数指针
F f(int); //错误,F是函数类型,而函数f1不能返回一个函数
F* f(int); //正确,显式地指定返回类型是指向函数的指针。

使用尾置返回类型声明返回函数指针的函数。

auto f(int)->int(*)(int*, int);

还可以直接声明:

int(*f(int)) (int*, int); //f(int)函数返回一个指向int(int*, int)类型函数的指针。

将auto和decltype用于函数指针类型

如果明确知道返回的函数是哪一个,就可以使用decltype简化书写函数指针返回类型的过程。

string::size_type sumLength(const string&, const string&);
string::size_type largerLength(const string&, const string&);

decltype(sumLength) *getFcn(const string&);

时刻注意decltype()作用于函数时,返回的是函数类型,而非指针类型,所以需要在getFcn前加上*表示函数的返回值是函数指针。

还有,decltype(expr)作用于表达式时,只是推断表达式的类型,而不实际计算其运算结果。

猜你喜欢

转载自blog.csdn.net/weixin_40313940/article/details/106245242
今日推荐