提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
开始
一、函数基础
函数是一个命名了的代码块,通过调用运算符(小括号)来执行函数,圆括号内使用逗号隔开的实参列表,我们用实参初始化函数的形参。
#include<iostream>
using namespace std;
int fact(int ival) {
int jval = 1;
while (ival != 1) {
jval *= ival--;
}
return jval;
}
int main() {
int kval = fact(5);
cout << kval << endl;
}
int jdz(int ival) {
int jval = abs(ival);
return jval;
}
int main() {
cout << jdz(-5) << endl;
}
静态局部对象:
第一次对象定义时初始化,知道程序结束才被销毁。局部静态对象默认值初始化为0.
#include<iostream>
using namespace std;
size_t count_calls() {
static size_t ctr = 0;
return ++ctr;
}
int main() {
for (int i = 0; i != 10; ++i)
cout << count_calls() << endl;
}
函数声明
函数声明与定义区别在于无函数体,包括三要素(返回类型,函数名,形参类型),可以确定函数接口。通常将函数声明放在头文件中以确保接口一致
int fact(int ival);
分离式编译
.cc生成.o文件(表示包含对象代码),然后由编译器将各个.o文件link在一起生成可执行文件。
二、参数传递
每次函数调用时都会重新创建它的形参,并用实参进行初始化。
与其它变量相同,形参类型决定了形参和实参交互的方式。如果形参是引用类型,他将绑定到实参上(passed by reference);否则,将实参的值拷贝后赋给形参(passed by value)。
传值参数
指针形参拷贝的是指针的值,此时可以间接修改指针指向的对象。
void reset(int *p)
{
*p = 0;
p=0;
}
int i = 42;
reset(&i);
cout << "i= " << i << endl; //输出i = 0;
#include<iostream>
using namespace std;
void sw(int * p1, int* p2) {
int ival = *p1;
*p1 = *p2;
*p2 = ival;
}
int main() {
int a = 0, b = 1;
sw(&a, &b);
cout << a << b << endl;
}
传引用参数
想要避免拷贝又不想改变对象内容是可以使用常量引用。(注:常量引用即可以用于被引用对象为常量,也可以用于被引对象为非常量)。
使用引用形参可以方便返回额外信息。
#include<iostream>
using namespace std;
string::size_type find_char(const string s, char c, string::size_type &occurs) {
auto ret = s.size();
occurs = 0;
for (decltype(ret) i = 0; i < s.size(); ++i) {
if (s[i] == c) {
if (ret == s.size())
ret = i;
++occurs;
}
}
return ret;
}
int main() {
string::size_type occ;
char c = 'e';
string::size_type ret = find_char("Werner", c, occ);
cout << "��һ�γ���" << c << "��λ��Ϊ��"<<ret << endl
<< c << "���ܳ��ִ���Ϊ��"<< occ << "��" << endl;
}
const形参和实参
与初始化的内容相同,注意不能用底层const初始化非const对象。尽量使用常量引用,因为形参为非常量只能接受非底层const实参,而常量引用则无此限制。
数组形参
数组有两个特殊性质:一、不允许拷贝数组。二、使用数组时通常会将其转化为指针。
以下三个函数完全等价的。
void print(const int*);
void print(const int[]);
void print(const int[10]); //代表我们期望有多少元素,实际不一定。
int i = 0,j[2] = {0,1};
print(&i);
print(j); //j转化成int*并指向j[0]
C++允许将变量定义成数组的引用,所以形参也可以是数组的引用。
void print(int (&arr)[10])
{
for(auto elem: arr)
cout << elem << endl;
}
练习
#include<iostream>
using namespace std;
/*
int fuc(int a, int* b) {
if (a > *b)
return a;
else
return *b;
}
int main() {
int a = 1;
int b = 0;
cout << fuc(a, &b) << endl;
}
*/
void fuc2(int** a, int** b) {
int* c = *a;
*a = *b;
*b = c;
}
int main() {
int a = 0, b = 1;
int* p1 = &a, * p2 = &b;
fuc2(&p1, &p2);
cout << *p1 << *p2;
}
#include<iostream>
using namespace std;
void print1(const int cp[]) {
cout << *cp << endl;
}
void print2(const int* beg,const int* end) {
while (beg != end)
{
cout << *beg++ << " ";
}
cout << endl;
}
int main() {
int i = 0, j[2] = { 0, 1 };
print1(&i);
print2(begin(j),end(j));
}
#include<iostream>
using namespace std;
void print(const int ia[10]) {
for (size_t i = 0; i != 10; ++i)
cout << ia[i] << endl;
}
int main() {
int i[10] = {};
print(i);
}
main
C++标准允许主函数main()有或没有参数列表。 能在主函数main()中使用一个或更多的参数。 如下是一些惯例:
int main(int argc, char *argv[])
第一个参数argc,指明有多少个参数将被传递给主函数main(),真正的参数以字符串数组(即第2个参数argv[])的形式来传递。每一个字符串均有自己意思。在这句法上面, 记住, argc代表参数的数量, main()函数本身是在索引0为的第一参数。 所以, argc总是至少为1 。它的总数是从argv列阵的元素数目。 这意味着, argv[0]的值是至关重要的。 如果用户在控制台环境中程序名称后键入含参数的指令, 那么随后的参数将传递给argv[1]。,第一个用于输入可执行文件名:
prog -d -o ofile data0
上面命令所提供的argv如下:
argv[0] = “prog”; //也可以指向一个空字符串
argv[1] = “-d”;
argv[2] = “-o”;
argv[3] = “ofile”;
argv[4] = “data0”;
data[5] = 0; //最后一个元素固定为0
练习
#include<iostream>
using namespace std;
void print(const char* c) {
string str;
while (*c)
{
str += *c++;
}
cout << str;
}
int main(int argc, char *argv[]) {
for(int i = 1;i < argc;i++)
print(argv[i]);
}
终端输入.\625.exe hellow world
含有可变形参的函数
有两种方法:1、initializer_list2、可变参数模板,见16章
initializer_list形参:数量未知但是全部实参的类型相同,与vector不同,initializer_list中的所有元素均为常量值。
向initializer_list中传递值序列,需要将他们放在一个花括号之内。
initializer_list<string> ls;
练习
#include<initializer_list>
#include<iostream>
using namespace std;
int sum(initializer_list<int> in)
{
int sum(0);
for(const auto &i:in){
sum+=i;
}
return sum;
}
int main()
{
int i(0);
i = sum({0,1,2,3,4,5});
cout<< i << endl;
}
省略符形参
C++程序访问C代码而设置。
void foo(parm_list,…) ;
void foo(…);
三、返回类型和return语句
return作用:终止当前正在执行的函数并将控制权返回调用函数的地方。
返回一个值的方式和初始化一个变量或形参的方式完全一样:函数的返回值用于初始化在调用函数处创建的临时对象。在求解表达式时,如果需要一个地方储存其运算结果,编译器会创建一个没有命名的对象,这就是临时对象。
与初始化一样,当返回值是引用类型时,调用函数和返回结果时返回的时候没有进行复制,返回的仅仅是所引对象的别名。注意不要返回局部对象的引用或指针,当函数执行完后,局部变量的存储空间会被释放,对局部对象的引用就会指向不确定的内存。
引用返回的是左值,因此可以进行修改。
练习
#include<iostream>
#include<vector>
using namespace std;
void print(const vector<int> &v,decltype(v.begin()) iter) ///注意形参一定要加引用,否者形参初始化的拷贝操作会让迭代器失效。
{
if (iter != v.end())
{
cout << *iter++ << " ";
return print(v,iter);
}
return;
}
int main()
{
vector<int> v{0,1,2,3,4,5,6,7,8,9,10};
print(v,v.begin());
}
返回数组指针
因为数组不能被拷贝,所以函数不能返回数组,但是可以返回数组的指针:
int(*fun(int i))[10];
使用类型别名可以简化这一任务:
typedef int arrT[10]; //类型别名,含有10个int的数组
using arrT = int[10]; //等价声明
arrT* func(int i);
或者使用尾置返回类型:
auto func(int i) -> int(*) [10];
使用decltype
int odd[] = {1,3,5,7,9};
decltype(odd) *arrPtr(int i)
{...}
练习
string (&fun())[10]{};
using arrS = string[10];
arrS (&fun()){};
auto fun() -> string (&) [10];
decltype string arrS[10];
arrS (&fun()){};
#include<iostream>
using namespace std;
int odd[] = {1,3,5,7,9};
int even[] = {0,2,4,6,8};
decltype(odd) &arrPtr(int i)
{
return (i%2)?odd : even;
}
int main()
{
decltype(odd) *arr;
arr= &arrPtr(6);
}
四、函数重载
1、同一个作用域内的几个函数名字相同但形参列表不同。
2、有无顶层const无法区分两个函数形参列表的不同,但是有无底层const可以区分不同函数形参。
3、const_cast与重载,如以下例子,参数和返回类型都是const string的引用,对非const使用时返回的仍然是const,不满足要求:
const string &shorterString(const string &s1, const string &s2)
{
return s1.size() <= s2.size() ? s1:s2;
}
所以需要一个非const string版本的函数:
string &shorterString(string &s1, string &s2)
{
auto &r = shorterString(const_cast<const string&> (s1), const_cast<const string&> (s2));
return const_cast<string&> (r);
}
4、如果在内层作用域内声明函数,它将隐藏外层作用域中声明的同名实体,即在不同的作用域中无法重载函数名。
五、特殊用途语言特性
三种函数相关的语言特性:默认实参、内联函数和constexpr函数。
默认实参
可以为函数指定默认的形参:
typedef string::size_type sz;
string screen(sz ht = 24,sz wid = 80, char background = " ");
注意一旦一个形参被赋予了默认值,它后面所有的形参都必须有默认值。
使用默认实参只需要调用的时候忽略就可以了,但需要注意只能忽略尾部的实参,同时形参可以相互转化的情况:
window = screen(, , "?"); //错误,只能忽略尾部的实参
window = screen("?"); //调用混乱,调用screen('?',80,'' )
第二个例子将?转化成将要sz的类型,与调用的意图相悖。
所以设计时尽量将不怎么使用默认值的形参放前面。
默认实参声明
函数声明通常放在头文件中,一个函数通常只声明一次。声明多次时,注意默认实参只能添加一次:
string screen(sz ,sz, char = " "); //表示只有第三个有默认值
string screen(sz ,sz, char = " "); //错误:重复声明
string screen(sz=24 ,sz=80, char);//正确:添加默认实参
练习
#include<iostream>
#include<string>
using namespace std;
string make_plural(size_t ctr, const string &word = "s",
const string &ending = "s")
{
return(ctr > 1) ? word + ending : word;
}
int main()
{
cout << make_plural(2) << endl;
}
内联函数和constexpr函数
内联函数:
内联函数可避免函数调用的开销,在返回类型前加上关键字inline即可,一般用于小型、频繁调用的函数。
问题:内联能提高函数的执行效率,为什么不把所有的函数都定义成C++内联函数?
答:如果所有的函数都是内联函数,还用得着“内联”这个关键字吗?内联是以代码膨胀(复制)为代价,仅仅省去了函数调用的开销,从而提高函数的执行效率。如果执行函数体内代码的时间,相比于函数调用的开销较大,那么效率的收获会很少。另一方面,每一处内联函数的调用都要复制代码,将使程序的总代码量增大,消耗更多的内存空间。
constexpr函数:
常量表达式(const expression)是指值不会改变并且在编译过程就能得到计算结果的表达式。显示,字面值属于常量表达式,用表达式初始化的const对象也是常量表达式。
一个对象(或表达式)是不是常量表达式由它的数据类型和初始值共同决定,例如:
const int max_num = 20; // max_num是常量表达式
const int limit = max_num + 1; // limit 是常量表达式
int staff_size = 2; // staff_size 不是常量表达式,因为staff_size没有用const修饰
const int sz = get_size(); // sz 不是常量表达式,虽然sz是个常量,但它的值在运行时才能确定
注意:constexpr函数可以触发编译时求值只是代表有这个能力,不一定返回常量表达式,需要同时满足:
1、所有参数、返回值都是常量表达式
2、返回的结果被用于常量表达式
才会触发编译时求值。同时该函数语句只允许一条return或者运行时不执行任何操作的语句(类型别名、using声明等)
constexpr指能用于常量表达式(见2.4.4节)的函数,需要注意该函数的形参与返回类型必须是字面值类型,且必须有且只有一条return语句。constexpr必须用常量表达式初始化。constexpr函数被隐式的指定为内联函数。
内联函数/constexpr函数通常定义在头文件中。
练习
#include<iostream>
using namespace std;
inline bool isShorter(const string& s1, const string& s2) {
return s1.size() < s2.size();
}
int main() {
const string s1 = "WERN", s2 = "ER";
cout << isShorter(s1, s2);
}
#include<iostream>
using namespace std;
constexpr bool isShorter(const string& s1, const string& s2) {
return s1.size() < s2.size(); //错误,const返回值要求常量表达式,它必须在编译时确定值得大小
}
int main() {
const string s1 = "WERN", s2 = "ER";
cout << isShorter(s1, s2);
}
调试帮助
程序包含一些只在开发时使用的调试代码时,可以使用两项预处理功能:assert和NDEBUG.
assert预处理宏:
标准形式:
assert(expr);
expr值为真,什么也不做。为假,输出信息并终止程序执行。
NDEBUG预处理变量
如果定义了NDEBUG预处理变量,则assert什么也不做。
此外,直接使用NDEBUG编写自己的条件检查代码。
#ifdef NDEBUG
...;
#endif
预处理器中几个对于程序调试有用的名字:
__FILE__ 存放文件名的字符串字面值
__LINE__ 存放当前行号的整形字面值
__TINE__ 存放文件编译时间的字符串字面值
__DATE__ 存放当前日期的字符串字面值
注意宏定义NDEBUG要在#include语句之前出现
#define NDEBUG
#include<iostream>
#include<vector>
#include<cassert>
using namespace std;
void print(const vector<int> &v,decltype(v.begin()) iter){
if (iter != v.end())
{
cout << *iter++ << " ";
cout << v.size() << " ";
return print(v,iter);
}
return;
}
int main()
{
vector<int> v{0,1,2,3,4,5,6,7,8,9,10};
assert(v.size() < 5);
print(v,v.begin());
}
六、函数匹配
练习
#include<iostream>
using namespace std;
void f() {};
void f(int i) {cout << i << endl;};
void f(int i, int j) {cout << i << " " << j << endl;};
void f(double i, double j= 3.14) {cout << i << " " << j << endl;};
int main() {
//f(2.56, 42); //错误,二义性调用
f(42);
f(42, 0);
f(2.56, 3.14);
}
六、函数指针
函数指针指向的是函数而非对象,函数的类型由它的返回类型和形参决定,与函数名无关。
bool lengthCompare(cconst string&, const string &);
该函数的类型是bool (cconst string&, const string &).
函数指针的声明只需要将函数名替换:
bool (*pf)(cconst string&, const string &);
函数名作为一个值使用时,自动转换成指针。
//这两句代码等效
pf = lengthCompare;
pf = &lengthCompare;
此外,还可以直接使用函数指针,无需解引用。
//这几句等效
bool b1 = pf("Hellow","Goodbye");
bool b1 = (*pf)("Hellow","Goodbye");
bool b1 = lengthCompare("Hellow","Goodbye");
函数指针可以作为形参使用,此时函数名可以自动转化为函数指针;可以使用typedef避免冗长。
同样,返回类型可以为函数指针,但是不能使用函数名,因为不会自动转换。
int(*f1(int))(int*,int);
该f1右边括号表示是个函数,左边*表示返回指针,外层右边括号表示指针指向函数,外层左边int表示返回int。
可以使用using避免冗长:
using F = int (int*,int);
using PF = int (*)(int*,int);
PF f1(int);
F *f1(int);
还可以使用尾置类型:
auto f1(int) ->int (*)(int*,int);
练习
#include<iostream>
#include<vector>
using namespace std;
int add(int a, int b) { return a + b; }
int subtract(int a, int b) { return abs(a - b); }
int multi(int a, int b) { return a * b; }
int divide(int a, int b) { return a / b; }
int Func(int a, int b); //vector<int (*)(int, int)> vec;
int main() {
vector<decltype(&Func)> vec;
vec.push_back(add);
vec.push_back(subtract);
vec.push_back(multi);
vec.push_back(divide);
int a = 10, b = 1;
for (auto& i : vec)
cout << (*i)(a, b) << endl;
}