1.函数的三个要求
- 提供函数定义
- 提供函数原型
- 调用函数
//一个简单的函数使用
//第一步 声明函数原型
void simple();
int main()
{
using namespace std;
//第三步 使用函数
simple();
return 0;
}
//第二步 定义函数
void simple()
{
using namespace std;
cout << "Shi Yuqi is so cool!";
}
2.定义函数
- 无返回值的函数
没有返回值的函数被成为 void 函数,它相当于是一个过程,或者子程序,一般用于执行一系列的操作,其通用格式如下:
void functionName(parameterList)
{
statement(s);
}
//一个小栗子
void apple(int n)
{
using namespace std;
for(int i = 0; i < n; i++)
{
cout << "apple" << endl;
}
}
参数列表意味着调用函数apple的时候 需要给参数进行赋值。
例如这个int类型的参数,在调用apple的时候,就需要传送一个int型的参数给他。
- 有返回值的参数
有返回值的参数会生成一个值,并将它返回个调用函数,其通用格式如下:
typeName functionName(parameterList)
{
statements;
return value;
}
注意:由返回值的函数必须要有返回语句,返回的值也可以是变量,可以是常量,甚至可以是表达式,但是返回的值一定要与函数头声明的返回值类型匹配。
注意:C++中返回值不能为数组。但是数组可以作为结构和对象的组成部分进行返回。
3.返回值是如何传递的
通常,函数会将返回值复制到指定的内存单元或指定的CPU寄存器中来将其返回,随后调用程序来查看该内存单元。返回函数和调用函数必须就该内存单元中存储的数据类型达成一致。函数原型负责告知调用程序返回值类型,函数头告知被调用函数返回什么数据类型。
4.函数原型
原型描述了函数到编译器的接口,函数原型将函数的返回值类型,参数类型和数量都告诉编译器。函数原型是一条语句,以分号结尾,其中包含函数返回值类型,函数参数的个数个类型。
注意:函数原型不需要提供参数的名称,只需要提供参数的类型即可。
double score(int);
//函数原型可以包含变量名 也可以不包含变量名
原型的优点
- 编译器可以正确处理函数返回值
- 编译器可以检查参数数目是否正确
- 编译器可以检查参数类型是否正确,如果不正确,可以试图将它变成正确的
5.函数参数和按值传递
C++通常按值传递参数,也就是将数值传递给函数,而后者将其赋给一个新的变量。
//举个小栗子
int size = 5;
apple(size);
//apple函数的函数头如下
void apple(int x)
/*
在被调用的时候,该函数创建了一个新的变量x,
用于存放从调用函数传递来的数值5,这里函数使用
的是size的副本,而不是原来的数据。
用于接受传递来的函数值的变量成为形参
传递给函数的值称为实参
C++中使用参数[argument]来表示实参
C++中使用参量[parameter]来表示形参
*/
注意:函数中声明的变量是函数私有的,也就是说,函数被调用的时候,计算机会给变量分配内存,函数调用结束后,计算机就会释放这部分内存。这种变量成为局部变量,他们被限制在函数内使用,也被成为自动变量。因为他们是被计算机自动分配内存和销毁的。(还记得之前说的三种类型吗 自动存储 外部存储 动态存储)
6.一个函数小程序
// 计算扑克牌中抽到六个选中的概率
#include<iostream>
long double probability(unsigned numbers,unsigned picks);
using namespace std;
int main()
{
unsigned int number,pick;
while((cin >> number >> pick ) && pick <= number)
{
cout << "You have a chance in "
cout << probability(number,pick);
cout << " of winning.\n";
cout << "Next two numbers (q to quit):";
}
return 0;
}
long double probability(unsigned numbers,unsigned picks)
{
long double result = 1.0;
unsigned n;
unsigned p;
for(n = numbers,p = picks; p > 0; n--,p--)
result = result * n / p;
return result;
}
7.函数和数组
现在我们如果思考去使用一个函数解决数组元素累加求和的问题,那么我们知道函数需要做的是计算总数,然后返回总数值,那么函数需要知道对哪个数组进行累计,同时还需要知道要累计多少量,也就是数组的长度,这两个量应该成为函数的参数。也就是如下:
//函数头应该如此声明
int appleNum (int arr[],int n)
但是需要注意的是,这里的方括号并不是指出arr是一个数组,而是一个指针!
这里可能就有小朋友问了,指针的声明方法不是这样的鸭,不是应该是如下:
int appleNum (int* num, int n)
实际上这两种方法均可实现目的,因为我们之前介绍过,数组名在C++中,绝大部分情况都可以视作数组元素的首地址,(目前我已知的是在使用sizeof()以及取地址运算符&进行计算的时候由不同)。而在函数原型或函数头中(注意只有这两个位置),int arr[] 和int* arr 表示的含义是相同的,都是传递指针,数组表示法[],是为了提醒阅读程序的人员,这不仅是一个指向int的指针,而且是指向数组第一个int元素的指针。
一般来说,当指针指向单个元素的时候,使用int* arr
当指针指向的是数组的时候,使用数组表示法 int arr[]
至于为什么这两个表示方法都可以使用方括号访问数组元素,这个就像我们之前介绍的一样,无论是指针,还是数组都可以通过方括号访问元素。这也是动态数组的实现方法。
//复习
//创建动态数组
int* ps = new int[55];
//删除动态数组
delete []ps;
传递数组实际上是传递指针意味着,调用函数传递给被调用函数的内容是数组的地址,包含元素的类型,元素的个数。有了这些信息之后,函数就可以使用传递来的数组了。这乍一看和之前讲过的C++参数按值传递由一定的差异,实际上这并不违反按值传递,只不过传递的值变成了地址的值,传递的仍然属于一个值,这个值依然被赋值给了一个新的变量,只不过它代表的是地址,而不是数组内容。
注意:这里传递的内容仅代表一个地址,它不代表数组首地址,也不能使用数组首地址的特性,sizeof()得到的结果仅代表地址的长度。
注意:因为传递的量是地址,int arr[] 只是int* arr的另一种表示方式而已,不能使用数组的表示方法。
例如 int arr[size]这样是不建议的。
使用const保护数组
在某些情况下,我们只是想要使用数组而已,并不希望在函数中对数组的值进行改变,这时候我们就需要使用const关键字,将指针指向的内容声明为常量数据,防止程序在不经意的时候对原始数据进行修改。
void show_array (const double arr[] ,int n)
//将arr声明为const类型,这样数组中的数据就不会被修改
8.一个小例子
//一个使用数组函数的完整房地产分析程序
#include<iostream>
using namespace std;
int fill_array (double arr[] ,int n);
void show_array (const double arr[] , int n);
void revalue_array (double r , double arr[] ,int n);
int main()
{
const int MAX = 5;
double houseValue[MAX];
int size = fill_array(houseValue, MAX);
show_array(houseValue, size);
if (size > 0)
{
double factor;
while (!(cin >> factor))
{
cin.clear();
while(cin.get() != '\n')
continue;
cout << "Enter a number:" ;
}
revalue_array(factor, houseValue, size);
show_array(houseValue,size);
}
return 0;
}
int fill_array (double arr[], int n)
{
double temp;
for(int i = 0; i < n; i++)
{
cout << "Please enter #" << i+1 << "value:";
cin >> temp;
if(!cin)
{
cin.clear();
while(cin.get() != '\n')
continue;
cout << "\nYou are a sunny boy!" << endl;
break;
}
else if (temp < 0)
{
cout << "\nGood Bye!" << endl;
break;
}
arr[i] = temp;
}
return i;
}
void show_array (const double arr[], int n)
{
for (int i = 0; i < n; i++)
{
cout << arr[i] << " ";
}
cout << endl;
}
void revalue_array (double r , double arr[] ,int n)
{
for (int i = 0; i < n; i++)
arr[i] *= r;
}
9.使用数组区间的函数
在C++的标准模板库STL中,使用了一种不同的方式来表示一个数组,通常我们会使用数组首地址和数组元素的长度来表示一个数组,而STL使用"超尾"的概念来确定区间,也就是指向数组结尾后面的一个位置作为尾部。具体来说就是如下:
//函数原型
int sum_arr(const int *begin, const int *end);
//使用方式
sum_arr(apple,apple+size);
10.const与指针
- 让指针指向一个常量对象,防止指针修改对象的值
//让指针指向一个常量对象,防止指针修改对象的值
int age = 39;
//代表pt指向的对象为const int pt不能修改这个值
//但是pt指向age,age并不是const,代表age可以修改39,而不能通过修改pt去修改39
const int *pt = &age;
//将常规变量地址赋值给常规指针
int *pt = &age; //可行
//将常规变量地址赋值给const指针
const int *pt = &age; //可行
//将const变量地址赋值给指向const指针
const int MAX = 5;
const int *pt =&MAX; //可行
//将const的地址赋值给常规指针
const int MAX = 5;
int *pt =&MAX; //不可行
之所以第四种情况不允许发生,是因为如果将const常量的地址赋值给一个常规指针,那么这个指针可以轻易的对常量的值进行修改,这导致const的状态很尴尬,因为C++禁止对常量值进行修改,所以也禁止了这种赋值方式。
- 将指针本身声明为常量,防止改变指针指向的位置
//让指针指向一个常量对象,防止指针修改对象的值
int age = 39;
const int *pt = &age;
//这里声明的const只能保证防止修改pt指向的值,但是没有办法防止修改pt的值
//也就是说,您可以给pt重新赋值,让他指向别的数据。
int height = 180;
pt = &height;
//这样*pt的值就变成了180,当然您仍然不能通过*pt来修改180的值
//那么如何防止pt修改它所指向的单元呢
//下面介绍一个新的语句
int* const finger = &height;
//const的位置只要稍微改变,代表的意思马上也截然不同
//第一个语句表示的是指向const int常量的普通指针
//第二个表示指向int的const常量指针
//这种表示方法允许使用*finger的方式来修改值
*finger = 250;
//当然只要你愿意 你可以将两个方法合二为一
const int* const pt = &height;
//这样就将上述两个特点全部具备
//你既无法修改常量值,也无法修改指向位置
通常将指针作为函数参数进行传递的时候,可以使用指向const的指针来保护数据,使用const就意味着不能对传递给他的数组的值进行修改,当然传递给他的数组不能为指针,或者指向指针的指针。
11.函数与二维数组
所谓的二维数组,也可以用普通的数组方式进行理解,例如数组 int arr[3][4]就可以看作是一个由三个元素的数组,数组每一个元素是一个拥有四个int型指针的数组。对于函数来说也是一样,将二维数组视为以为数组来进行定义即可,具体定义方法如下:
//二维数组作为参数的函数原型
int show_arr2 (int arr[][4], int size);
//这里的列数要写在第一个参数中,而不是在多加一个列的参数
//原因是,指针类型指出这是一个指向由4个int组成的数组的指针
//我猜这样系统才能知道进行地址加减的时候要移动多少个字节
注意:像二维数组这样指向指针的指针是没有办法使用const关键字的!!
12.函数和C风格的字符串
所谓的C风格字符串,那其实就是char型的数组。区别在于字符串有特定的结尾标志,也就是‘\0’。C风格字符串作为参数三种表示方法如下:
- 使用双引号表示字符串
int size = strlen("Merry");
- 使用char型数组表示字符串
char word[50] = "Merry";
int size = strlen(word);
- 使用char型指针表示字符串
char* word = "Merry";
int size = strlen(word);
之所以函数不需要像之前的数组那样由一个参数专门传递数组元素个数是因为字符串由它独特的结束符号,也就是我们之前说的字符数组成为字符串的秘诀,‘\0’。函数可以通过判断‘\0’的位置来确定是否已经读完了字符串内容,所以就不需要另一个传递数组元素个数的参数了。
//处理字符串的标准方式
while(*str) //*str可以判断是否字符串已经走到了'\0'的位置
{
statements
str++;
}
13.C风格字符串作为返回值
因为我们之前介绍过,数组类型是没有办法作为返回值的,所以函数也没有办法返回一个字符串,但是可以返回字符串的地址,而且这样效率会更高。
//一个返回字符串的简单函数
#include<iostream>
using namespace std;
char* build_str (char n, int num);
int main()
{
int nums = 5;
char alpha = 'c';
char* ps = bulid_str(alpha, num);
cout << ps <<endl;
delete [] ps;
return 0;
}
char* build_str (char n, int num)
{
//使用new方法创建动态数组,元素个数的表达式可以为变量
//但是普通方法创建数组是没有办法使用变量表达式的 只能为整型常量表达式
char* pr = new char[n+1];
pr[n] = '\0';
while(n-- > 0)
{
pr[n] = c;
}
return pr;
}
14.函数与结构
函数传递结构要比传递数组简单的多,因为结构变量的行为更接近于单值变量,结构可以通过等号赋值,同样,也可以按值传递结构,函数也可以返回结构。不过于数组不同的是,结构名只是结构的名称,而不是结构的首地址,如果想要获得结构的地址还是需要使用取地址运算符&。但是按值传递很大的结构的时候,不可避免的会因为结构内容量较大而导致系统内存要求增大,处理速度降低。所以有一些人更倾向使用传递结构地址的方式来传递结构。当然C++还有一种引用传递的方式,这将在之后介绍,首先说前两个方式。
- 按值传递返回结构
#include<iostream>
using namespace std;
struct fruit
{
int apple;
int banana;
};
fruit fruitSum (fruit f1, fruit f2);
void showFruit (fruit f);
int main()
{
fruit f1 = {15, 20};
fruit f2 = {20, 30};
showFruit(f1);
showFruit(f2);
showFruit(fruitSum(f1, f2));
}
fruit fruitSum (fruit f1, fruit f2)
{
fruit fsum;
fsum.apple = f1.apple + f2.apple;
fsum.banana = f1.banana + f2.banana;
return fsum;
}
void showFruit (fruit f)
{
cout << f.apple << endl;
cout << f.banana << endl;
}
fruit就好像是一个而标准的类型名,可以被用来声明变量,作为函数的返回类型和参数类型,注意C++函数是按值传递,也就是showFruit(fruitSum(f1, f2));中showFruit()函数使用的是fruitSum()函数的返回值,而非函数本身。
- 传递结构的地址
- 调用参数是,将结构的地址而不是结构本身传递给他
- 将形参声明成一个fruit类型的指针,即fruit*,因为函数不需要修改结构,所以声明为const类型
- 由于形参是指针而非结构,所以要使用简介成员运算符(->)这与new创建的动态结构相类似。
//通过地址传递的结构
void showFruit (const fruit* pt)
{
cout << pt -> apple << endl;
cout << (*pt).banana << endl;
}
15.函数和string对象
string对象与char型数组不同,与结构类似,它也可以通过等号赋值,同样的,string类型的对象作为一个实体进行参数传递,如果需要字符串数组,可以将他声明成string类型的对象数组,而非char型的二维数组。下面是string类型函数的例子:
#include<iostream>
#include<string>
using namespace std;
const int MAX = 5;
void display(const string words[], int n);
int main()
{
string list[MAX];
for(int i = 0; i < n; i++)
{
getline(cin, list[i]);
}
display(list, MAX);
return 0;
}
void display(const string words[], int n)
{
for(int i = 0; i < n; i++)
{
cout << words[i] << endl;
}
}
16.函数和array对象
//复习 C++11中用来替代数组的两个模板类
//vector类 相当于动态数组 使用new delete方式 存放在堆或者自由存储区
//使用方式
#include<vector>
vector<int> ti(10);
//vetcor<typeName> vt(n_elem); n_elem可以是整型常量 也可以是整型变量
//array类 相当于是正常数组的替代品 存放在栈中 长度固定 效率比数组高一些
//使用方式
#include<array>
array<int, 5> arr = {1, 2, 3, 4, 5};
//array<typeName, num> arr;
//函数如何将array对象作为参数
#include<iostream>
#include<string>
#include<array>
using namespace std;
const int Seasons = 4;
const array<string, Seasons> Snames =
{"Spring", "Summer", "Fall", "Winter"};
void fill (array<double, Seasons> *pa);
void show (array<double, Seasons> da);
int main()
{
array<double, Seasons> expenses;
fill(&expenses);
show(expenses);
return 0;
}
void fill (array<double, Seasons> *pa)
{
for(int i = 0; i < Seasons; i++)
cin >> (*pa)[i];
}
void show (array<double, Seasons> da)
{
double total = 0.0;
for(int i =0; i< Seasons; i++)
{
total += da[i]
cout << da[i] << " ";
}
cout << total << endl;
}
17.递归
(这么多天来第一个图片)
//一个基础的递归程序
#include<iostream>
using namespace std;
void countdown (int n);
int main()
{
countdown(4);
return 0;
}
void countdown (int n)
{
cout << n << endl;
if (n > 0)
countdown(n-1);
cout << "wuhu" << n << endl;
}
18.函数指针
与数据项相似,函数也拥有地址,所谓函数的地址就是存储其机器语言代码的内存的开始地址。而函数指针有什么用呢?我们可以编写将另一个函数的地址作为参数的函数,这样我们就可以通过一个函数找到另一个函数并且运行它,与直接调用另一个函数相比,这样可能会比较笨拙,但是他能够做到不同的时间运行不同函数这件事。
举个例子:我们想编写一个函数,帮助每个程序员测试他们函数执行所需要的时间,为了实现这个目标。我们必须完成以下几个工作:
- 获取函数的地址
- 声明一个函数指针
- 使用函数指针调用函数
①如何获得函数的地址呢:只需要使用该函数名即可,如果apple()是一个函数,那么apple就代表该函数的地址。如果想要将函数作为参数传递,那么就一定要使用函数名称作为参数。
//传递返回值
show(sum(f1, f2))
//传递函数
show(sum)
②声明一个函数指针:声明指向某种数据类型的指针时,必须指定指针所指向的类型,声明函数指针也是一样,也就是说声明应该想函数原型一样指出有关函数的信息。
//函数原型
double sum(double f1, double f2);
//函数指针声明
double (*pf)(double,double);
//这里pf就是函数指针
//赋值
//正确的声明函数指针后,应该给他进行赋值
pf = sum;
//编写一个函数参数类型是函数指针的函数原型
void estimate(int lines, double (*pf)(int));
③使用指针来调用函数
double pam(int);
double (*pf)(int);
pf = pam;
double x = pam(4);
double y = (*pf)(5); //这种写法更醒目的提示我们 这是一个函数指针
//C++也允许像使用函数名一样使用pf
double z = pf(6);
19.函数指针数组
函数指针的表示可能很长很恐怖,下面来介绍几个小栗子:
//函数原型
const double * f1(const double ar[], int n);
const double * f2(const double [], int n);
const double * f3(const double *, int n);
//这三个函数原型看似不同,实则一样,含义完全相同
//原因由二
//第一 函数原型只要求声明类型,并不要求声明标识符
//第二 数组名传递的就是首地址 这和指针没什么不同
//现在要定义一个函数指针指向三个变量之一
const double * (*pf)(const double*, int);
//当然也可以进行初始化
const double * (*pf)(const double*, int) = f1;
//当然C++11还提供了巨他喵无敌简单方法---自动类型推断
auto pf = f1;
//不过自动类型推断只适用于单值初始化
//对于列表初始化,自动类型推断是没有办法完成的
//例如我现在要写一个函数指针数组,来指向上面三个函数
const double * (*pa[3])(const double*, int) = {f1,f2,f3};
//变量名pf,先向右看,说明pf是个数组,遇到括号,往左看,说明这个数组的元素是指针
//跳出括号,发现外面还是括号,说明这数组元素指针类型是个函数指针
//最后这就是个函数指针数组
//当然如果我们拥有了pa就可以适用自动类型推断来声明同类型的数组了
auto pb = pa;
//如何适用这种数组来调用函数呢
//因为数组类型是函数指针数组,所以每一个数组元素都是一个函数指针
//所以将数组元素按照函数指针的适用方法就可以调用函数了
const double* x = pa[1](av,3);
const double* y = (*pb[1])(av,3);
//如果再恐怖一点呢
//现在创建一个指针指向刚刚我们创建的数组
//数组名是指向数组首元素的指针
//数组元素是指向函数的指针
//现在要创建一个指向函数的指针的指针的指针
//我们仍然可以适用自动类型推断帮我们简单完成
auto pp = &pa; //云淡风轻
//如果要自己写可就没有这么云淡风轻了
const double* (*(*pa)[3])(const double, int)
//如果要使用pa的话
//pa是一个指针 指向数组
//*pa 代表数组名
// (*pa)[] 代表数组的元素
//数组的元素 是 函数指针
const double* x = (*pa)[1](ar,6);
复杂定义的右左法则:
从变量名看起,先往右,再往左,碰到圆括号就调转阅读的方向;括号内分析完就跳出括号,还是先右后左的顺序。如此循环,直到分析完整个定义。
- type (var)(…); // 变量名var与结合,被圆括号括起来,右边是参数列表。表明这是函数指针
- type (var)[]; //变量名var与结合,被圆括号括起来,右边是[]运算符。表示这是数组指针
- type (*var[])…; // 变量名var先与[]结合,说明这是一个数组(至于数组包含的是什么,由旁边的修饰决定)
20.typedef简化
//使用typedef创建别名
typedef const double real;
//标识符进行声明
typedef const double* (*(*pa)[3])(const double, int);
//这样pa 就代表了这一大长串定义
pa f = f1;