C++ Primer 第七章 函数
7.1 函数的定义
函数是完整的一个逻辑代码块。 函数接收一些参数并在函数体内做处理。函数可以返回运算结果也可能不返回任何内容。
C++有两类函数 无返回值的void函数 和 有返回值函数。
7.2 参数传递
Ø非引用形参
参数是以复制形式传递的,函数内部对参数修改不一会对调用函数的实参产生影响。看下面代码
{
i = 3;
s = "new";
}
int it = 2;
string str = "old";
cout << it << str << endl; // 输出: 2 old
funct(it, str);
cout << it << str << endl; // 输出还是: 2 old
调用函数funct时传递的是实参的拷贝,虽然在函数内更改了参数值但不影响外部实参。
需要注意的是指针。在函数内对指针参数本身(指针的指向地址)的更改同样不会影响调用实参。不过对指针指向的内容更改却是有影响的。
{
*i = 3; // 对指向内容做更改
}
int it = 2;
string itpr = ⁢
cout << itpr << *itpr << endl; // 输出: 000ffxx0 2
funct(itpr);
cout << itpr << *itpr << endl; // 输出: 000ffxx0 3
函数对指针本身做的更改(指向地址)不会反应到外部实参。 指向内容做更改对外部实参是有影响的,实际上两个指针指向同一内容,任意一个指针所做的更改对其他指针都有反应。
函数体内两个表达式顺序执行对结果有很大影响。若先改变指针指向再改变指向内容值,实际上就表示两个指针指向了不同对象,这时修改指向内容对外部实参没有丝毫影响。
有些时候我们希望传递的参数不允许修改,在参数前增加 const 关键字就可以实现。
void function(const int i)
{
i = 2 ; // 错误,不允许修改常量参数
}
指针比较特殊。指针操作有两个含义:指针指向更改和指向内容更改。这个常量修饰符实际修饰的是指向内容,指向地址还是可以更改的 如下代码:
void function(const int *i)
{
*i = 2 ; // 错误,不允许更改指向内容
i = 0 ; // 允许更爱指针指向地址
}
Ø引用形参
非引用传参方式有个很大的弊端,当需要传递一个很大的参数时拷贝参数副本开销比较大。如果我们要求函数对参数修改能反映到外部的实参时非引用方式也无能为力。
这个时候可以使用引用类型传参。
{
i = 0 ;
j = 0 ; // 不允许更改
}
int it = 2;
int jt = 1;
cout << it << jt << endl; // 输出: 2 1
funct(it, jt);
cout << it << jt << endl; // 输出: 0 1
it以引用方式传递,函数内部修改 i 实际上就是在修改 it 本身。 如果限制函数更改可以增加 const 修饰。j 参数经过修饰后内部不允许做修改。
对于指针来说指针引用(*&parname)形参在函数体内指向更改或者指向内容更改都会反映到外部实参:
{
p = 1100ffxx ; // 修改指向
*p = 6 ; // 修改指向内容 注意此时修改的是新地址的内容值
}
int it = 2;
int pr = ⁢
cout << pr << *pr << endl; // 输出: fff110xx 2
funct(pr);
cout << pr << *pr << endl; // 输出: 1100ffxx 6
函数形参可传递实参一般分为 字面值常量, 普通常量,变量。不同情况下可传递的情况不同
}
int i(1);
const int j(1);
funct(15); // 传递字面值
funct(i); // 传递变量
funct(j); // 传递常量
也可以const限定参数 void funct(const int p) 三类参数传递已然正确,唯一的区别是限定后函数体内不允许更改参数 p
非const引用形参:
void funct(int &p)
{
}
int i(1);
const int j(1);
funct(3); // 不允许传递字面值
funct(i); // 传递变量
funct(j); // 不允许,函数内部可以修改参数值,而参数是常量时修改操作是是非法的
const引用形参:
void funct(const int &p)
{
}
int i(1);
const int j(1);
funct(1); // 传递字面值
funct(i); // 传递变量
funct(j); // 传递常量
应该掌握每种情况下可传递的实参类型。
对于容器参数如果按拷贝传递性能会大大降低。可以传递引用。 但时间操作中传递容器的迭代器会更加方便。迭代器类似于数组指针。
数组也可以作为新参,不过数组比较特殊因为数组不允许赋值,所以数组新参会被转换为对应指针。
void funct(int *i) ; // 推荐写法,表明参数是一个指向数组的指针
void funct(int[]) ;
void funct(int[10]) ;
这三种方式是一样的,实际等价于 void funct(int *i)
第三种形参定义设置了数组大小,但是实际调用中可以传递大小不等于10的数组,因为形参定义的数组大小会被忽略。 所以在函数中的操作不能依赖形参定义的大小。
如果想严格匹配实参和形参数组大小可使用 引用数组,定义方式如下:
void funct(int (&arr)[10]) ; // 表示是一个数组引用,且数组大小是10 。 圆括号必须因为下标操作具有更高优先级
void funct(int &arr[10]) ; // 错误的语法
函数操作数组不但可以如上传递还能可以传递指针,数组大小等参数类型
void funct(int *bej, int *end)
{
for(int *i = bej; i != end; i++)
{
cout << *i << endl;
}
}
int it[]{1,9,7,0,1};
funct(it, it + 5); // 注意:末端哨兵位是最后一位的后一位
// 传递第一位和数组大小
void funct(int *bej, size_t len)
{
for(size_t i = 0; i < len; i++)
{
cout << bej[i] << endl; // 数组指针下标操作等价于 *(beg + i)
}
}
size_t len = 5;
int it[len]{1,9,7,0,1};
funct(it, len);
7.3 函数返回值
函数可以没有返回值(void函数)也可以有返回值。同参数一样函数可返回多种类型。
Ø返回非引用类型
这种情况和形参类似,返回的是对象副本。
{
return 1 ; // 返回字面值副本
return a ; // 返回参数副本
int b(1) ;
return b ; // 返回局部变量副本
}
Ø返回引用类型
返回引用表示可以对返回值做赋值操作。
int &funct(int &a)
{
return a;
}
int i(1);
int &j = funct(i); // 也可以这样操作 funct(i) = 3
cout << i << j << endl // 输出 3 3 j 是 i 的引用
// 可以返回const引用
const int &funct(int &a)
{
return a;
}
int i(1);
int &j = funct(i); // 错误,不能用const引用初始化非const引用
const int &j = funct(i);
j = 3; // 错误,不能为常量赋值
千万记住:不管任何时候都不允许用const引用初始化非const引用(或者说不允许让非 const 引用指向一个const对象)
返回值遵循一个安全约束,即 不允许返回局部对象的引用或指针
int &funct(int a, int &b)
{
int c = 1;
return c; // 标准局部对象 不允许返回
return a; // 传参时拷贝的实参,实际值只有在函数内有效,不允许返回
return b; // 通过引用形参 传递的是外部实参的引用,可以传递
}
// 返回指针
int *funct(int a, int *b)
{
int c = 1;
return *c; // 标准局部对象 不允许返回
return *a; // 传参时拷贝的实参,实际值只有在函数内有效, 不允许返回
return b; // 传参时拷贝的实参,虽然指针是局部对象但他指向的值是外部数据,可以传递
}
总之判断返回是否是局部变量可以看返回值的实际值是作用范围否只在函数体内。
7.4 函数声明
函数调用时还未定义则需要声明。函数声明和变量声明类似可查看相关内容。
函数形参可以定义默认值,调用函数时如果不提供参数值则使用默认值
{
}
funct() // 等同于 funct(1,"val", " ")
funct(3) // 等同于 funct(3,"val", " ")
funct(3,"new") // 等同于 funct(3, "new", " ")
7.5 局部对象
局部对象的作用范围限制在函数内,函数执行完毕局部变量会被系统自动销毁。
不过静态局部对象不会销毁,所有静态对象一旦创建就会一直存在直到应用程序关闭。
静态对象初始化只会执行一次:
{
static int a(0); // 只会执行一次,第二次执行到代码处会检查静态变量 a 是否存在若存在则跳过
a++;
cout << a << endl;
} // 函数到此执行完毕并清理局部对象,但不会清理静态对象,所以a依然存在,值为最后操作结果。 a虽然不会被清理但只限在定义的函数体内访问
funct() ; // 输出 1
funct() ; // 输出 2
funct() ; // 输出 2
7.6 内联函数
内联函数是指编译时将调用函数的地方用实际函数体语句替代的一类函数,函数定义时返回值前加上 inline 就表示函数内联
inline bool funct(int a, int`b)
{
return a > b;
}
bool c = funct(a, b) ; // 编译时实际会被替换为 bool c = (a > b);
内联函数有较好的性能,因为函数在调用时系统刚要分配栈空间,内联函数会直接展开代码,所以不会有栈空间分配步骤 (内联函数体内数据需要的空间会在调用它的函数执行时一并分配)。
内联函数的特点决定了如果修改函数则所有用到内联函数的地方都要重新编译。
7.7 类的成员函数
类成员函数和普通该函数没有本质区别。
类成员函数包含一个隐藏参数 this 指针。它指向成员函数所属的当前类对象。
类成员函数名后面可以用const修饰,其作用是表示 this 指针指向常量,也就是说不允许修改当前类的任何成员
{
public:
int a ;
void funct1() const
{
a = 1; // 错误,等价于 this->a = 1 由于函数后面const修饰 this 指针指向了常量所以无法修改
}
void funct2()
{
a = 1; // 允许,等价于 this->a = 1
}
}
类成员函数比较多,比如普通成员函数,构造函数,拷贝函数,析构函数等等,会在后面详解。
7.8 函数重载
函数重载是指返回类型相同,函数名相同但参数不完全相同的多个函数。函数调用时会根据传递的参数类型和个数寻找最合适的重载函数。
不但参数类型和个数可以作为重载依据,当形参数是引用或指针时 const 可用作重载依据。
// 引用形参时 是否是const
const int b;
通常来说函数以参数类型不同或参数数量来重载。
7.9 指向函数的指针
指针不但可以指向内置类型,类类型,数组,还可以指向函数。函数指针的最大作用是将函数作为一种类型传递。
定义语法是:
void (*pr)(int i, string s) ; // 定义了一个函数指针,指向的函数无返回值,并且有两个参数一个是int类型另外一个string类型
pr = funct ; // 为定义的指针赋值(funct是函数名称)
pr(1,"str") ; // 通过指针调用函数(不用解引)
可以用 typedef 可简化定义,将定义一个变为定义一类
typedef void (*pr)(int i, string s) ; // 定义了一类函数指针,指向的函数无返回值,并且有两个参数一个是int类型另外一个string类型
pr f1 = funct ; // 定义类型变量
f1(1,"str") ; // 通过指针调用函数
在这两种方式中,指针都可以赋0值,表示还未指向任何函数。
最后我们看看实际中函数指针的应用
// 形参是函数指针 返回函数指针
pr getfunction(pr f,int`a)
{
f(a); // 执行
return f; // 返回函数指针
}
pr f1 = getfunction(funct, 1);
f1(2);
定义参数时用了typedef的类型pr,如果不使用则定义语句相当复杂。
对于有重载的函数,定义的指针一定要有对应的重载版本。 初始化指针变量时系统也会查找对应的重载函数
void fun(string a);
typedef void (*pr)(string); // 定义了string形参版的函数指针
pr p = fun; // 定义函数指针变量,系统会用 void fun(string) 版函数来初始化p