c++primer 第六章 函数

6.1 函数基础

  • 函数的返回类型不能是数组或函数,但是可以是指向数组或者函数的指针。

6.1.1 局部对象

  • 块是一个局部作用域,会隐藏块外的同名局部变量
  • 所有函数体外定义的对象在程序启动时创建,在程序结束时销毁。
  • 局部对象在定义语句时创建,在块末尾销毁。
  • 自动对象的初始化
    • 变量定义时含有初值,用该初始值进行初始化
    • 否则进行默认初始化,可能产生未定义的值。
  • 局部静态对象,在程序第一次经过对象定义语句时初始化,直到程序终止才销毁。

6.1.2 函数声明

  • 函数放在头文件
    能保证同意函数所有的声明一致,需要修改时,修改一条声明就好;放在源文件中合法,但是容易出错。
  • 含有函数声明的头文件应该包含到源文件中。

6.1.3分离式编译

  • 分离式编译可以在修改单独某一个文件时使用。
    g++ fact.cpp factMain.cpp生成一个a.out。修改factMain.cpp中的内容后,需要重新使用该命令全部重新编译链接。
    实际上,可以分成编译、链接两部分
g++ -c fact.cpp #编译,生成fact.o
#若修改factMain.cpp执行以下三步就可以了
g++ -c factMain.cpp #编译,生成factMain.o
g++ fact.o factMain.o #链接,生成a.out可执行文件
g++ fact.o factMain.o  -o main #链接,生成main可执行文件

6.2 函数传递

  • 形参是引用类型时,实参被引用传递(或函数被传引用)调用
  • 实参值需要拷贝给形参时,实参被值传递(或函数被传值)调用。形参和实参相互独立,在函数块内拷贝构造实参(靠拷构造在第13章)。

6.2.1 传值参数

c++中建议使用引用类型代替指针

6.2.2 传引用参数

  • 使用引用避免值拷贝
    因为拷贝大的类在不必要地消耗资源,并且有的类并不支持拷贝操作,如iostream
  • 如果函数无须改变引用形参的值,最好声明为常量引用
  • 使用引用实参可以返回额外信息

6.2.3 const形参和实参

  • 当实参初始化形参时,会忽略掉顶层const,但是会保留底层const。
    即若传入的指针(或引用)是顶层const的,其原来不能重新指向(或表示)其他变量,在函数体内没有这个限制,可以重新指向(或表示)其他变量。
  • 可以使用非常量初始化一个底层const对象,但是反过来不行
  • 在声明函数时,尽量使用常量引用
    把不会改变的形参定义为普通引用是一个错误,会误导函数调用者这个函数会改变实参的值,并且会限制函数接受的实参类型
int find_char(string& s);
string s("无限~");
const string cs("罗小黑");
find_char(s);//可以调用
find_char(cs);//不能用const初始化一个非常量,编译时报错

find_char2(const string& s);
find_char2(s);
find_char2(cs);//都可以正常调用

6.2.4 数组形参

  • 数组以指针的形式传递给函数。
    即使在函数内,程序员也要保证数组不能越界,可以使用三种方法告诉函数数组的大小。
    • 数组本身含有一个标记符,如字符数组可以以\0为标记。
    • 以数组首元素和尾后元素指针为参数
    • 传递一个表示数组大小的形参
//这种形式是允许的,但是也是危险的,编译器会把ia转换为指针类型
//在传入大小不为10的数组不会报错
void print(const int ia[10]){
    for (size_t i = 0;i<10;i++){
        cout << ia[i] << endl;
    }
}
int main(){
    int a[5]={1,2,3,4,5};
    print(a);//情况未知
}
  • 允许函数参数定义数组的引用,数组维度也是参数类型的一部分
//函数只能作用于大小为10的int数组
//&arr两侧必须加上括号,int &arr[10]是引用的数组,而非数组的引用
void fun(int (&arr)[10]) 
  • 若需要将多维数组作为函数参数,函数第二维(及以后所有维度)都是数组的一部分,不能省略。
//matrix是指向含有10个整数的数组的指针
void fun(int (*matrix)[10],int rowsize);
//等价定义
void fun(int matrix[][10],int rowsize);

6.2.5 main处理命令行选项

有时需要给main传实参,如告诉函数将要执行的操作。
假设程序在可执行文件prog中,希望向程序输入以下选项

prog -d -o ofile data0

main可以声明为以下的形式

int main(int argc,char** argv);

argv是一个数组,元素是c风格字符串指针
argc是argv中元素个数。
若输入命令是prog -d -o ofile data0,argc为5,argv[0]到argv[4]依次是prog -d -o ofile data0。argv[5]为0。

argv[0]是程序名,argv[1]才是可用实参

6.2.6 含有可变形参的函数

  • 两种主要方法,一种不常用方法来处理不同数量的实参的函数
    • 主要方法1:实参类型相同,使用标准库类型,initializer_list
    • 主要方法2:实参类型不同,可变参数模板 (16.4节详讲)
    • 非常用:省略符形参,只用于与c函数交互的接口程序
  • initializer_list
    在这里插入图片描述
    • initializer_list对象中的元素永远是常量
  • 省略符形参
    • 省略符形参是为了访问某些特殊的c代码,这些代码使用了名为varargs的c标准库功能。
    • 省略符形参仅用于c和c++通用的类型,并且大多数类类型在传递给省略符形参时都无法正确拷贝
    • 形式有以下两种
void foo(para_list,...);//指定了部分形参类型,对指定类型会执行正常的类型检查,且逗号符是可选的
void foo(...);//省略符形参对应的实参无须类型检查

6.3 返回类型和return语句

6.3.1 无返回值函数

可以使用return;或者 return void;

6.3.2 有返回值的函数

  • 在含有return语句的循环语句后面也应该有一条return语句,如果没有的话,这个程序就是错误的。但是很多编译器都无法发现这个错误。
bool cmp_test(const string& str1,const string& 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   ;//错误,但编译器可能发现不了
        }
    }
}
  • 不要返回局部对象的引用和指针或者返回未定义的的值。
    在返回时,一般是返回值的拷贝,这是安全的;但仅仅返回一个指向对局部对象的指针或引用的拷贝,这就是危险的。
  • 调用运算符的优先级和点运算符与箭头运算符优先级相同,并且符合左结合律。
    所以可以直接调用返回指针或类的对象
shorterString(s1,s2).size()
  • 返回一个引用的函数得到左值,其他类型得到右值
  • 可以使用初始化列表返回值
vector<string> fun(){
	//使用返回的初始化列表初始化vector
	return {"string1","string2","string3"};
}
  • main的返回值被看作状态指示器,返回0表示成功,其他表示失败。
    • 非0值的具体含义依机器而定
    • main函数不可以递归调用自己
    • cstdlib头文件中定义了预处理变量表示成功失败
int main(){	
	if(some_fail) return EXIT_FAILURE;
	else return EXIT_SUCCESS;
}

6.3.3 返回函数指针

  • 定义一个返回数组指针的函数,数组的维度必须跟在函数名字之后
//函数定义
Type (*function(parameter_list))[dimension]
//fun这个函数返回数组指针,这个指针指向有10个int类型的数组
int (*fun(int i ))[10];
//fun(int i )			表示函数需要一个int型实参
//(*fun(int i ))		表示可以对函数进行解引用操作
//(*fun(int i ))[10]    表示引用操作得到一个大小为10的数组
//int (*fun(int i ))[10]表示数组中元素为int类型
  • 使用尾置返回类型
auto fun(int i)->int(*)[10]
  • 使用decltype
int arr[]={0,1,2,3,4,5,6,7,8,9}
decltype(arr) *fun(int  i);

6.4 函数重载

  • 重载函数:函数名字相同,形参列表不同。(无法识别不同的返回值)
  • main函数不能重载
  • 顶层const形参无法和另一个没有顶层const的形参区别开,但是可以识别底层const(形参时指针或引用,可以区分其指向常量对象和非常量对象实现重载)
Record lookup(Phone);
Record lookup(const Phone);//无法和Record lookup(Phone)区分

Record lookup(Phone*);
Record lookup(const Phone*);//正确,与Record lookup(Phone*)不同
  • 最好只重载那些非常相似的操作。
  • const_cast在函数中的使用
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);
}

  • 函数调用的三种结果
    • 编译器找到实参的最佳匹配,生成该函数的代码。
    • 找不到合适的实参的最佳匹配,编译器法抛出无匹配的错误
    • 有多于一个函数可以匹配,但每一个都不是最佳选择,编译器发出二义性调用错误

6.4.1 重载与作用域

  • 将函数声明放在局部作用域中很不明智,以下只是说明c++特性,并不建议使用
    在不同作用域中,函数就不是被重载而是隐藏
void print(string str);
int main(){
	//在新作用域中声明不好
	void print(int ival);
	//错误,因为string 为参数的函数已经被隐藏
	print("ni hao");
}

6.5 特殊用途语言特性

6.5.1 默认实参

  • 一旦某个形式被赋予了默认值,它后面的所有形参都必须要有默认值
  • 通常,应该在函数声明中指定默认实参,并将该声明放到合适的头文件中
  • 局部变量不能作为默认实参。只要表达式的类型能够转换成形参所需的类型,这个表达式就能作为默认实参。
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,'*')
	}

6.5.2 内联函数和constexpr函数

  • 内联函数
    • 内联函数会在调用点展开,避免函数调用的开销
    • 在函数返回值前加上inline就可以实现
    • 内联说明只是向编译器发出一个请求,编译器可以忽略这个请求
    • 内联机制一般用于优化规模较小、流程直接、调用频繁的函数。
    • 很多编译器不支持内敛递归函数
  • constexpr函数
    • constexpr函数是指能用于常量表达式的函数
    • constexpr函数需要遵守的要求:
      • 返回值类型及所有形参的类型都是字面值类型
      • 函数体内有且只有一条返回语句。
      • 若存在其他语句,这些语句不能执行任何操作,如空语句、类型别名、using声明。
    • constexpr函数隐式被指定为内联函数
  • 内联函数和constexpr函数放在头文件

6.5.3 调试帮助

可以使用assertNDEBUG的预处理功能来调试代码

  • assert预处理宏
    • assert(expr),若expr为真,assert啥也不做;否则,assert输出信息并终止程序执行。
    • assert宏定义在cassert头文件中
    • 预处理名字由预处理器而不是编译器管理,因此可以不用using声明
    • 含有cassert头文件的程序,不能使用名为assert的变量、函数或其他实体。
      即使没有直接包含cassert头文件,也有可能间接包含了该头文件,因此最好不要使用名为assert的变量、函数或其他实体
  • NDEBUG预处理变量
    • 定义了 NDEBUG,则assert什么都不做
    • 使用NDEBUG的方法
      • 在程序中使用#define NDEBUG
      • 在编译时加入命令,如CC -D NDEBUG main.cpp
    • 自己使用NDEBUG 编写条件调试
void print(){
	#ifndef NDEBUG
	//__func__是编译器定义的静态局部常量,用来存放函数名字
	cerr<<__func__<<"debugging"<<endl;
	#endif
	...
}
  • 编译器除了定义 __func__外,还定义了如下变量
    • __FILE__存放文件名的字符串字面值
    • __LINE__存放当前行号的整型字面值
    • __TIME__存放文件编译时间的字符串字面值
    • __DATE__存放文件编日期时间的字符串字面值

6.6 函数匹配

  • 第一步:确定候选函数。候选函数与被调用函数同名、在调用点可见。
  • 第二步:确定可行函数,可行函数形参和本次调用提供的实参数量相当、每个实参的类型与对应的形参类型相同或能转换成形参类型
  • 第三步:确定最佳匹配

在调用重载函数时,应该尽量避免强制类型转换。若需要强制类型转换,说明设计的形参集合设计不合理。

6.6.1 实参类型转换

  • 实参到类型的转换分为五个等级
    • 精确匹配
    • 通过const转换实现的匹配
    • 通过类型提升实现的匹配
    • 通过算术类型转换或指针转换实现的匹配
    • 通过类类型转换实现的匹配

6.7函数指针

  • 函数指针指向的是函数非不是对象,其类型由返回类型和形参类型确定,与函数名无关。
  • 函数指针声明:用指针替换函数名即可
//函数声明
bool lenghthCmp(const string& s1,const string& s2);
//函数指针声明,(*pf)的括号不可少,否则就是返回指向bool的指针
bool (*pf)(const string& s1,const string& s2);
//以下赋值等价,取地址符时可选的
//但是函数原型、返回值要和pf声明完全一致,精确匹配
pf = lenghthCmp;
pf = &lenghthCmp;

//以下都是等价的调用
bool b1=pf("hello","goodbye");
bool b2=(*pf)("hello","goodbye");
bool b1=lenghthCmp("hello","goodbye");
  • 函数指针可以作为函数的参数,实参可以直接使用函数名。函数作为参数会隐式转换为函数指针。
  • 可以使用decltype、类型别名来简化声明函数指针
//相同的声明
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,lenghthCmp)

//fun1 和fun2 是函数类型
typedef bool fun1(const string &,const string &);
typedef decltype(lenghthCmp) fun2;
//funp1 和funp1 是指针类型
typedef bool (* funp1)(const string &,const string &);
typedef decltype(lenghthCmp) *funp1;
  • 返回指向函数的指针
    函数指针作为返回类型和作为参数不一样。
    函数作为参数会自动转为指针,但作为返回值不会自动转换
using F = bool(const string &,const string &);//函数类型
using Fp = bool(*)(const string &,const string &)//指针类型,(*)的括号也是必要的
//也可以用尾置返回类型
auto Fp2(const string &,const string &)->bool(*)(const string &,const string &);
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值