函数基础知识
要使用C++函数,需要完成以下工作:
- 提供函数定义;
- 提供函数原型;(通常被隐藏在include文件中)
调用函数;
#include<array>
#include<iostream>
using namespace std;
// 函数原型
int caculate_area(int w, int h); // 可以省略w和h
int main(){
int w = 10;
int h = 20;
// 函数调用
cout << caculate_area(w, h);
}
// 函数定义
int caculate_area(int w, int h){
return w * h;
}
注意事项:
C++对于返回值类型有一定的限制:不能是数组;(对于输入没有这样的限制)
- 除了数组之外,其它任意类型:整数、浮点数、指针、结构体、对象等;
- 当数组作为结构或者对象的组成部分时,仍旧可以返回!
int caculate_area(int, int);
仍旧合法;
函数返回值时,首先将返回值复制到指定的CPU寄存器或内存单元中,随后调用程序将查看该内存单元。函数的原型将返回值类型告知调用程序,而函数定义命令被调用函数应该返回什么类型的数据。
当遇到多个返回语句时,第一个有效!
原型的意义
原型描述了函数到编译器的接口,也就是说,它将函数返回值的类型以及参数的类型和数量告诉编译器。
有了函数定义了,为什么不直接利用函数定义来获取这些信息呢?一个原因是这样做效率不高,另一个原因是函数定义可能存在于其它文件,C++容许将一个程序放在多个文件中,单独编译这些文件,最后再将他们组合起来(后面这个原因没有很懂)。
基于原型以上意义,我们不难推断,在数组作为参数进行传递时,被调函数中的参数数组不会重新开辟空间,也就是相对当一个const指针的传递(事实上的确是一个指针)。指针做参数,仍旧是值传递,只是传递的值时地址。
#include<array>
#include<iostream>
using namespace std;
// 函数原型
int caculate_area(int w[], int n);
int main(){
int w[5] = {10, 10, 10, 10, 10};
int n = 5;
// 函数调用
cout << caculate_area(w, n) << endl;
cout << w[0] << endl;
cout << w[1] << endl;
return 0;
}
// 函数定义
int caculate_area(int w[], int n){
int total = 0;
for(int i=0; i < n; i++){
total += w[i];
w[i] = total;
}
return total;
}
以上代码会修改w
数组的值!(同样非数组的类型,将引用作为参数,同样可以修改参数原值)
原型的功能
- 原型确保编译器正确处理函数返回值;
- 原型确保编译器检测使用的参数数目是否正确;
原型确保编译器检查使用的参数类型是否正确,如果不正确,则将进行类型转换。
函数和数组
可以参考上面的代码,值得注意的就是原型中的数组表示:
typeName varName[]
;并且此变量是一个指针,由于指针的[]
索引,其和数组看不出差别;
以下两句等价:int caculate_area(int w[], int n);
int caculate_area(int* w, int n);
对数组进行sizeof得到的是整个数组块的大小,对指针sizeof则是一个类型长度;
- 函数调用传递数组实际上是传递指针,sizeof满足指针特性;
- 对于不需要修改的数组,前面最好加上
const
关键字(例如上面第一个原型中的int w[]
); 对于数组的传递,不仅需要传递数组的首地址,还需要传递元素个数;
利用
const
关键字修饰指针并非一定是指向常量,上述代码中number
如果不是常量,np
仍旧可以用const
修饰;- 只是修饰之后不能通过
np
改变被指向变量的值; 非
const
指针不能指向const
变量;const int number = 10;
int* np = &number; // Invalid
也就是说:
const指针可以指向非const变量;
- 非const指针不能指向const变量;(指向之后则可以进行修改,与
const
不符) - const指针可以指向const变量;
-
常量指针
注:此处常量指针才是指针值不能改变的指针!
常量指针在定义时必须被初始化,因为指针值不能修改!int number = 100;
int * const np = &number;
上述代码表示,
np
是一个常值指针,它的值不能修改,只能指向number
,也就是说const
的位置,实际影响变量的特性。由此还可以定义指向常量的常量指针:const double pi = 3.1415926;
const double* const pPI = π
函数和二维数组
首先应该知道,二维数组由一维数组组成。
int data[3][4] = {{1,2,3,4},{2,3,4,5},{3,4,5,6}};
int sum = sum_data(data, 3);
// 或者 sum_data(data, 3, 4)
那么函数的原型如何定义呢?
对于data来说,data[0],data[1],data[2]都是一个包含4个元素的一维数组。显然,data可以看做是一个一维数组,数组里面的元素还是一维数组。根据函数不能将数组作为参数,所以形参必须是指针类型(因为data传递的是地址),并且指针指向的元素是包含4个元素的一维数组,由此有:int (*ar2)[4]
,式中ar2
是一个指向4个元素的一维数组的指针。int sum_data(int (*ar2)[4], int size);
其中的
int (*ar2)[4]
又可以等价于int ar2[][4]
(意义相同,形式不同,但是后者更明了!)错误理解
显然如果不限定二维数组的列数,那么可以有:
_int (*ar2)[]_
也可以写作_int *(*ar2)_
。int sum_data(int **ar2, int row, int col);
以上的写法是错误的,因为在参数传递过程中,传递的是
data
的首地址,如果不指定data
元素的类型(即4个元素的一维数组),那么如何知道ar2 + 1
为多少呢?也即是说,int ** ar2
没有提供足够充足的信息!
那么如何理解int** p
呢?—> 指向整型指针的指针!(复合指针)也就是说*p是一个整型指针,那么p是指向整型指针的指针!高维数组
int data[3][3][3] = {{{1,2,3},{1,2,3},{1,2,3}},
{{1,2,3},{1,2,3},{1,2,3}},
{{1,2,3},{1,2,3},{1,2,3}}};
int sum = sum_data(data, xxx);
data[i]是一个指针,类型为整型;data[i][j]同样是指针,类型同样为整型;如果但看data则,它是一个数组,包含3个元素,每个元素都是一个二维数组。对比二维数组,易知:
int sum_data(int (*data)[3][3], int size);
// 等价于
int sum_data(int data[][3][3], int size);
其中,还可以利用一级指针通过计算偏移量来进行访问(数组空间是连续的,线性;这种方式的好处是:不是针对特定size的数组,更具有通用性!!!):
//通过一级指针,计算偏移量来遍历二维数组:
void printDoubleDimensionalArray(int *array,int rows,int cols)
{
int i,j;
for(i=0;i<rows;i++)
{
for(j=0;j<cols;j++)
{//通过一级指针,计算偏移量来遍历二维数组:
printf("%4d",*(array+i*cols+j));
}
printf("\n");
}
}
函数和C-style字符串
字符串表示方式:
char数组(必须以\0结尾);
- 引号括起的字符串常量;
- char指针;
将字符串作为参数,和数组一样都是传递的地址。值得注意的是,char数组只有以\0结尾才能被称为是字符串,而\0(编码值为0)又可以作为遍历字符串的结束标志!
函数和结构
结构作为参数编写函数时比数组作为参数编写函数简单很多,因为结构可以作为参数进行传递(其主要原因是数组不能互相进行赋值操作)。
struct person
{
std::string name;
int age;
};
person choose_person(person p1, person p2); // 函数原型
person per1 = {"dhh", 20};
person per2 = per1;
per2.name = "zyr";
cout << "person 1:" << per1.name << endl;
cout << "person 2:" << per2.name << endl;
person choose_person(person p1, person p2){
//TODO
}
与数组不同,结构名和地址无关,如果需要获取结构体的地址,需要利用取地址运算符&
。
结构体完全可以向普通变量那样使用,作为参数,作为函数返回值,但是当结构体较大时,结构体的复制等会增加内存消耗,降低运行速度,所以可以采用指针进行参数传递。
此处不再赘述函数和结构的案例等。值得注意的是:
- 结构指针访问结构成员:
sp -> member
或者(*sp).member
; - 结构访问结构成员:
struct.member
;函数和string对象
string对象和C-style的字符串用途几乎相同,但是它和结构更为相似。对象之间可以进行赋值(会生成副本)。 ```cppinclude
include
using namespace std; const int SIZE = 3; void show_strings(std::string sa[], int n);
int main(){ std::string sa[SIZE]; cout << “Enter “<< SIZE << “strings!” << endl; for(int i=0;i<SIZE;i++){ cout << i << “: “; getline(cin, sa[i]); } show_strings(sa, SIZE); }
void show_strings(std::string sa[], int n){ cout << “display below:” << endl; for(int i=0; i<SIZE;i++){ cout << i << “: “; cout << sa[i] << endl; } }
<a name="A8YND"></a>
## 函数和array对象
和string差不多
```cpp
const std::array<std::string, 2> Names = {"yangtao", "songjiazhuang"}
递归
递归:自己调用自己。利用递归时可能会造成冗余计算 -> 斐波那契数列的递归算法;利用递归时,应该进行一定的分析,以防冗余计算!
def get_fibo(int n){
// 递归出口
if(n == 0 || n == 1)
return 1;
// 递归调用
else
return get_fibo(n-1) + get_fibo(n-2);
}
函数指针
函数地址是存储其机器语言代码的内存开始地址。利用函数的地址,可以编写一个函数,它以另一个函数的地址作为参数,这样前者就可以找到后者并运行它。当然,通常情况下,还是直接调用函数比较方便。以函数地址为参数,适合不同情况下调用不同函数的情况。
函数指针调用函数:
- 获取函数的地址;
- 申明一个函数指针;
-
1、函数地址获取
获取函数地址很简单,直接用函数名即可。例如上面的
get_fibo()
函数,get_fibo
函数名就是该函数的地址(与数组类似)。
由此有如下总结: 数组和函数的名称即是地址;
-
2、声明函数指针
声明指向某种数据类型的指针时,必须指定指针指向的类型。同样,声明函数指针时,也必须指定指针指向的函数类型。这意味着声明应指定函数的返回类型以及函数的特征标(参数列表)。换言之,声明应像函数原型那样指出有关函数的信息。
double pam(int); // 函数原型
double (*pf)(int); // 定义指针
pf = pam; // pf指向pam函数
由于
pam
是函数,与之对应的(*pf)
也是函数,从而pf
是函数指针。
注意事项: (*pf)
括号不能掉;- 没了括号:
double *pf (int);
表示的是pf是返回指针类型的函数;
- 没了括号:
pf
和pam
的特征标必须相同;(参数列表和返回值类型必须相同)double pam(int); // 定义原型
double (*pf)(int);// 定义指针
pf = pam;
double x = pam(4);
double y = (*pf)(4); // 也容许直接用:pf(4)来调用函数
if(x==y){
cout << "OK!" << endl;
}
double pam(int a){
return a;
}
注意事项:
严格来说
(*pf)
才是函数,但是在C++中允许利用pf(4)
的形式去使用指针;也可以在声明函数指针时进行初始化:
double (*pf)(int) = pam;
double estimate(int lines, double (*pf)(int)); //函数指针作为参数
3、函数指针数组
只有当函数的特征标相同时才能构成数组!
const double* (*pa[3])(const double *, int) = {f1, f2, f3};
值得注意的是:
[]的优先级高于,所以`pa[3]`首先说明pa是一个数组,包含三个元素,然后指明元素的类型为指针(pa是指针数组);
- 所以是
*pa[3]
的形式!
- 所以是
- 相反如果是:
(*pa)[3]
,则首先说明pa是指针,然后表示(*pa)是包含3个元素的数组,那么pa就是数组指针,该指针指向的数组元素必须为3;(数组指针) 注意C++中数组是可以越界访问的!例如:
int a[2] = {1,2};
cout << a[3] << endl;
#include<array>
#include<iostream>
using namespace std;
double add(double, double);
double add1(double, double);
void calculate(double, double, double (*ap)(double, double));
int main(){
double (*ap)(double, double) = add; // ap是指针
calculate(2.1, 2.2, ap);
calculate(2.1, 2.2, add1); // add1是函数名,实际上是函数地址值
}
double add(double x, double y){
return x + y;
}
double add1(double x, double y){
return (x + y)*10;
}
void calculate(double x, double y, double (*ap)(double, double)){
cout << (*ap)(x, y) << endl;
}
auto进行简化
const double* (*pa[3])(const double *, int) = {f1, f2, f3};
上述代码不能使用:
auto pa = {f1, f2, f3}
;auto只能用于单值初始化,而不能用于初始化列表;
- 像:
auto pc = pa
则是合理的;
- 像:
**&pa==*pa==pa[0]
typedef进行简化
typedef可用于别名的创建,例如给double
创建别名real
;
其可以减少代码的输入量!
将别名当做标识符进行声明:typedef double real;
typedef const double *(*p_fun)(const double *, int); // p_fun现在是类型名了
p_fun p1 = f1;