【C&C++】 函数参数详解(含指针)
一、C语言函数声明
C语言代码由上到下依次执行,原则上函数定义要出现在函数调用之前,否则就会报错。但在实际开发中,经常会在函数定义之前使用它们,这个时候就需要提前声明。
所谓声明(Declaration),就是告诉编译器我要使用这个函数,你现在没有找到它的定义不要紧,请不要报错,稍后我会把定义补上。
函数声明的格式
函数声明的格式非常简单,相当于去掉函数定义中的函数体,并在最后加上分号;,如下所示:
dataType functionName( dataType1 param1, dataType2 param2 ... );
也可以不写形参,只写数据类型:
dataType functionName( dataType1, dataType2 ... );
#include <stdio.h>
//函数声明
int sum(int m, int n); //也可以写作int sum(int, int);
int main(){
int begin = 5, end = 86;
int result = sum(begin, end);
printf("The sum from %d to %d is %d\n", begin, end, result);
return 0;
}
//函数定义
int sum(int m, int n){
int i, sum=0;
for(i=m; i<=n; i++){
sum+=i;
}
return sum;
}
我们在 main() 函数中调用了 sum() 函数,编译器在它前面虽然没有发现函数定义,但是发现了函数声明,这样编译器就知道函数怎么使用了,至于函数体到底是什么,暂时可以不用操心,后续再把函数体补上就行。
二、指针
数据在内存中的地址也称为指针,如果一个变量存储了一份数据的指针,我们就称它为指针变量。
指针变量赋值的方式一般是把变量的地址取出来,然后赋值给对应类型的指针变量。
指针常见初始化
int a;
int *p;
p = &a;
int a,b;
int *p = &a, *p2 = &b;
声明一个指针变量并不会自动分配任何内存。在对指针进行间接访问之前,指针必须进行初始化:或是使他指向现有的内存,或者给他动态分配内存
,否则我们并不知道指针指向哪儿,这将是一个很严重的问题,
/* 方法1:使指针指向现有的内存 */
int x = 1;
int *p = &x; // 指针 p 被初始化,指向变量 x ,其中取地址符 & 用于产生操作数内存地址
/* 方法2:动态分配内存给指针 */
int *p;
p = (int *)malloc(sizeof(int) * 10); // malloc 函数用于动态分配内存
free(p); // free 函数用于释放一块已经分配的内存,常与 malloc 函数一起使用,要使用这两个函数需要头文件 stdlib.h
常见指针变量的定义
定 义 | 含 义 |
---|---|
int *p; | p 可以指向 int 类型的数据,也可以指向类似 int arr[n] 的数组 |
int **p; | p 为二级指针,指向 int * 类型的数据 |
int *p[n]; | p 为指针数组。[ ] 的优先级高于 *,所以应该理解为 int *(p[n]) |
int (*p)[n]; | p 为二维数组指针 |
int *p(); | p 是一个函数,它的返回值类型为 int * |
int (*p)(); | p 是一个函数指针,指向原型为 int func() 的函数 |
三、常用的传参方式
在C语言中可用的有 传值、传地址两种方式;
C++语言中还有引用方式
示例解释
#include<stdio.h>
void Swap1( int *i, int *j)
{
int t;
t=*i;
*i=*j;
*j=t;
}
void Swap2( int i, int j)
{
int t;
t=i;
i=j;
j=t;
}
void Swap3( int *i, int *j)
{
int *t;
t=i;
i=j;
j=t;
}
int main()
{
int a = 3,b = 4;
point_1 = &a;
point_2 = &b;
Swap1(point_1,point_2);
}
其中,
只有 Swap1 能确实的改变a和b的值;
Swap2 交换的是形参i,j的值,a和b的值不受影响;
Swap3 虽然得到了a和b的地址,但是只是将形参i,j存放的地址进行了交换,a和b的值不受影响;
总结:变量本身的地址是不可以改变的,只能通过指针变量得到实参的地址,然后通过实参的地址去改变实参的值;
根据上述情况,C++ 中出现了 引用 方式。
1. 值传递
将变量的值传递给函数的形式参数,实际就是用变量的值来新生成一个形式参数,因而在函数里对形参的改变不会影响到函数外的变量的值。
2. 地址传递
将变量的地址赋给函数里形式参数的指针,使指针指向真实的变量的地址,因为对指针所指地址的内容的改变能反映到函数外,能改变函数外的变量的值。
3. 引用传递
众所周知,函数的参数是作为局部变量,对局部变量的操作不会影响外部的变量,如果想要修改传入的参数,只能使用指针。 在 C++ 中,存在“引用”可以不使用指针,从而达到修改传入参数的目的。
对引用变量对的操作就是对原变量的操作
形参相当于是实参的“别名”,对形参的操作其实就是对实参的操作,在引用传递过程中,被调函数的形式参数虽然也作为局部变量在栈中开辟了内存空间,但是这时存放的是由主调函数放进来的实参变量的地址。被调函数对形参的任何操作都被处理成间接寻址,即通过栈中存放的地址访问主调函数中的实参变量。正因为如此,被调函数对形参做的任何操作都影响了主调函数中的实参变量。
#include<iostream>
using namespace std;
//值传递
void change1(int n){
cout<<"值传递--函数操作地址"<<&n<<endl; //显示的是拷贝的地址而不是源地址
n++;
}
//引用传递
void change2(int & n){
cout<<"引用传递--函数操作地址"<<&n<<endl;
n++;
}
//指针传递
void change3(int *n){
cout<<"指针传递--函数操作地址 "<<n<<endl;
*n=*n+1;
}
int main(){
int n=10;
cout<<"实参的地址"<<&n<<endl;
change1(n);
cout<<"after change1() n="<<n<<endl;
change2(n);
cout<<"after change2() n="<<n<<endl;
change3(&n);
cout<<"after change3() n="<<n<<endl;
return true;
}
输出结果:
实参的地址0x6dfeec
值传递--函数操作地址0x6dfed0
after change1() n=10
引用传递--函数操作地址0x6dfeec
after change2() n=11
指针传递--函数操作地址 0x6dfeec
after change3() n=12
分析:
可以看出,实参的地址为 0x6dfeec
采用值传递的时候,函数操作的地址是 0x6dfed0 并不是实参本身,所以对它进行操作并不能改变实参的值;
再看引用传递,操作地址就是实参地址 ,只是相当于实参的一个别名,对它的操作就是对实参的操作
引用传递与地址传递的本质区别
注意:要把引用的 & 跟取地址符 & 区分开来,引用不是取地址的意思
本小节摘抄至:
https://www.iteye.com/blog/xinklabi-653643
从概念上讲。指针从本质上讲就是存放变量地址的一个变量,在逻辑上是独立的,它可以被改变,包括其所指向的地址的改变和其指向的地址中所存放的数据的改变。
而引用是一个别名,它在逻辑上不是独立的,它的存在具有依附性,所以引用必须在一开始就被初始化,而且其引用的对象在其整个生命周期中是不能被改变的(自始至终只能依附于同一个变量)。
在C++中,指针和引用经常用于函数的参数传递,然而,指针传递参数和引用传递参数是有本质上的不同的:
指针传递参数本质上是值传递的方式,它所传递的是一个地址值。值传递过程中,被调函数的形式参数作为被调函数的局部变量处理,即在栈中开辟了内存空间以存放由主调函数放进来的实参的值,从而成为了实参的一个副本。值传递的特点是被调函数对形式参数的任何操作都是作为局部变量进行,不会影响主调函数的实参变量的值。(这里是在说实参指针本身的地址值不会变)
而在引用传递过程中,被调函数的形式参数虽然也作为局部变量在栈中开辟了内存空间,但是这时存放的是由主调函数放进来的实参变量的地址。被调函数对形参的任何操作都被处理成间接寻址,即通过栈中存放的地址访问主调函数中的实参变量。正因为如此,被调函数对形参做的任何操作都影响了主调函数中的实参变量。
引用传递和指针传递是不同的,虽然它们都是在被调函数栈空间上的一个局部变量,但是任何对于引用参数的处理都会通过一个间接寻址的方式操作到主调函数中的相关变量。而对于指针传递的参数,如果改变被调函数中的指针地址,它将影响不到主调函数的相关变量。如果想通过指针参数传递来改变主调函数中的相关变量,那就得使用指向指针的指针,或者指针引用。
指针和引用的相同点和不同点:
相同点:
- 都是地址的概念;
- 指针指向一块内存,它的内容是所指内存的地址;而引用则是某块内存的别名。
不同点:
- 指针是一个实体,而引用仅是个别名;
- 引用只能在定义时被初始化一次,之后不可变;指针可变;引用“从一而终”,指针可以“见异思迁”;
- 引用没有 const,指针有 const,const的指针不可变;(具体指没有 int& const a 这种形式,而const int& a是有的, 前者指引用本身即别名不可以改变,这是当然的,所以不需要这种形式,后者指引用所指的值不可以改变)
- 引用不能为空,指针可以为空;
- “sizeof 引用”得到的是所指向的变量(对象)的大小,而“sizeof 指针”得到的是指针本身的大小;
- 指针和引用的自增(++)运算意义不一样;
- 引用是类型安全的,而指针不是 (引用比指针多了类型检查
https://blog.csdn.net/qq_18628523/article/details/90728446
四、函数参数为数组
函数参数为一维数组
指针与数组
数组名称也作为数组的首地址使用。
int a[10];
int *p = a;
上述代码中, a作为数组a的首地址 &a[0] 被赋值给指针变量 p,同理 a+i 等同于 &a[i]
C语言函数参数为数组的书写规范:
-
形参数组和实参数组的长度可以不相同,因为在调用时,只传送首地址而不检查形参数组的长度。
-
在函数形参表中,允许不给出形参数组的长度,
void nzp(int a[])
也可以用指针来代替:
void nzp(int *a)
注:可以理解函数的参数为数组时,相当于将数组的首地址传入,即地址传递。这种情况下,在函数中对数组元素的修改等同于是对原数组元素的修改。
函数参数为二维数组(三种方法)
二维数组:
当作一维数组的每一个元素都是一个一维数组
例如,定义为 int a[5][6] 的二维数组看作五个长度为6的一维数组。
方法1:在参数声明中指定二维数组的列数
void foo(int a[][3])
方法2:把参数声明为一个指向数组的指针
#include <stdio.h>
void foo(int (*a)[3], int m, int n) {
int i = 1;
int j = 1;
printf("a[%d][%d]=%d\n", i, j, a[i][j]);
}
int main() {
int a[2][3] = {
{1,2,3},
{4,5,6}
};
foo(a, 2, 3);
}
函数的参数声明改成了:
int (*a)[3]
这个声明的含义是:
声明(*a)是一个数组,等价于int b[3]时,b是一个数组。
推出a是一个指针,指向一个数组,类似a是指针,指向b的。
方法3:把参数声明为指向指针的指针
#include <stdio.h>
void foo(int **a, int m, int n) {
int i = 1;
int j = 1;
printf("a[%d][%d]=%d\n", i, j, a[i][j]);
}
int main() {
int a[2][3] = {
{1,2,3},
{4,5,6}
};
int * p[3];
p[0] = a[0];
p[1] = a[1];
p[2] = a[2];
foo(p, 2, 3);
}
这里要注意的是指针的指针,和二维数组的差异;二维数组的地址是连续的,所有成员按顺序排序;而指针的指针只要求指针地址连续,而不要求指针的指针地址连续。
然后作为实参传递时,也不能直接使用a传递,因为类型不匹配,必须定义新的变量 p,然后把 a 的值赋给 p,再传递给 foo() 函数。
C++字符串变量(string)作为函数形参传递(引用方式)
#include <iostream>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
using namespace std;
void loadpfont1(string &filename)
{
filename = filename + "\\now";
cout<<filename<<endl;
}
void loadpfont2(const char* str)
{
cout<<str;
}
int main()
{
string path = "E:\\data\\TEST";
//
loadpfont1(path);
const char *str = path.c_str();//c_str()函数返回一个指向正规C字符串的指针常量
loadpfont2(str);
return 0;
}
输出结果:
E:\data\TEST\now
E:\data\TEST\now