C语言学习记录(五):指针和数组(下)
来源:互联网 发布:process monitor 知乎 编辑:程序博客网 时间:2024/06/05 04:30
摘要: 本文继续介绍了C语言的一些指针和数组的知识,包括指针数组和数组指针,多维数组和多维指针,数组参数和指针参数分析和函数与指针分析。
活着不一定平安,平安不一定快乐。
——《失去的森林》许达然
指针数组和数组指针
数组类型
工作中的对话:
A: 这里定义的数组什么类型的?
B:int型……
C语言中的数组有自己特定的类型
数组的类型由元素类型和数组大小共同决定
例: int array5的类型为int5
定义数组类型:
C语言中通过typedef为数组类型重命名
typedef type(name)[size];
数组类型:
typedef int(AINT5)[5];typedef float(AFLOAT10)[10];
数组定义:
AINT5 iArray; //定义了一个元素类型为int,元素个数为5的数组iArrayAFLOAT10 fArray; //定义了一个元素类型为float,元素个数为10的数组fArray
数组指针
数组指针用于指向一个数组
数组名是数组首元素的地址, 但并不是数组的起始地址
通过将取地址符&作用于数组名可以得到数组的起始地址
可通过数组类型定义数组指针 :ArrayType *pointer;
也可以直接定义:type (*pointer)[n];
pointer为数组指针变量名
type为指向的数组的类型
n为指向的数组的大小
int array[10];int *PA = array;
这里PA是一个整形指针,指向数组首元素的地址,一个int型指针指向一个int型元素,所以PA不是数组指针,是普通指针。
数组指针可以这么定义:
ArrayType *pointer;int array[5];pointer = &array;
也可以简写为:int(*pointer)5;
实例分析
数组类型的定义
数组指针的使用
数组指针运算
#include <stdio.h>typedef int(AINT5)[5];typedef float(AFLOAT10)[10];typedef char(ACHAR9)[9];int main(){ AINT5 a1; float fArray[10]; AFLOAT10 *pf = &fArray; ACHAR9 cArray; char(*pc)[9] = &cArray; char(*pcw)[4] = cArray; int i = 0; printf("%d, %d\n", sizeof(AINT5), sizeof(a1)); for(i=0; i<10; i++) { (*pf)[i] = i; } for(i=0; i<10; i++) { printf("%f\n", fArray[i]); } printf("%0X, %0X, %0X\n", &cArray, pc+1, pcw+1); //pc+1 <==> (unsigned int)pc + 1*sizeof(*pc)}$ gcc ctest.cctest.c: 在函数‘main’中:ctest.c:14: 警告:从不兼容的指针类型初始化$ ./a.out 20, 200.0000001.0000002.0000003.0000004.0000005.0000006.0000007.0000008.0000009.000000BFCC590B, BFCC5914, BFCC590F$
虽然第14行有警告,但还是能编译通过,因为虽然我们将数组元素首地址赋值给了数组指针,类型不一样,但还是可行的。
pc+1为&cArray的值加9(sizeof(*pc)=9),pcw+1为cArray的值加4。
指针数组
指针数组是一个普通的数组
指针数组中每个元素为一个指针
指针数组的定义:type *pArray[n];
type *
为数组中每个元素的类型
pArray为数组名
n为数组大小
实例分析
关键字查找
#include <stdio.h>#include <string.h>int lookup_keyword(const char* key, const char* table[], const int size){int ret = -1;int i = 0;for(i=0; i<size; i++){ if( strcmp(key, table[i]) == 0 ) { ret = i; break; }}return ret;}#define DIM(a) (sizeof(a)/sizeof(*a))int main(){const char* keyword[] = { "do", "for", "if", "register", "return", "switch", "while", "case", "static"};printf("%d\n", lookup_keyword("return", keyword, DIM(keyword)));printf("%d\n", lookup_keyword("main", keyword, DIM(keyword)));}$ gcc ctest.c$ ./a.out 4-1$
可以看到”return”找到了,而”main”没有找到。
main函数的参数
main函数可以理解为操作系统调用的函数
在执行程序的时候可以向main函数传递参数
//常用写法int main()int main(int argc)int main(int argc, char *argv[])int main(int argc, char **argv)int main(int argc, char *argv[], char *env[])int main(int argc, char **argv, char **env)
argc – 命令行参数个数
argv – 命令行参数数组
env –环境变量数组
命令行参数的使用
#include <stdio.h>int main(int argc, char* argv[]){int i = 0;printf("============== Begin argv ==============\n");for(i=0; i<argc; i++){ printf("%s\n", argv[i]);}printf("============== End argv ==============\n");}
$ gcc ctest.c$ ./a.out ============== Begin argv ==============./a.out============== End argv ==============$
我们发现它打印了自己本身,那么我们加几个参数呢
$ ./a.out endice "I love you"============== Begin argv ==============./a.outendiceI love you============== End argv ==============$
发现我们加的参数也都打印出来了,利用main函数的这个特性我们完全可以自己写一些命令,例如删除命令,我们只要把文件名作为参数传进去,利用C语言的文件操作就可以删除了。我们就可以自己写自己喜欢的命令了。(这里顺便提一下linux的alias命令,这个命令的功能是给已有的命令重新命名,例如alias mk=”mkdir” , 我们就可以使用mk命令来创建文件夹了,事实上linux中的ll命令也是这么产生的alias ll='ls -l --color=auto'
)
我们在main函数中加入第三个参数,这个参数是操作系统的环境变量数组,因为main函数时操作系统调用的,所以可以把操作系统的环境变量打印出来:
#include <stdio.h>int main(int argc, char* argv[], char* env[]){int i = 0;printf("============== Begin argv ==============\n");for(i=0; i<argc; i++){ printf("%s\n", argv[i]);}printf("============== End argv ==============\n");printf("\n");printf("\n");printf("\n");printf("============== Begin env ==============\n");for(i=0; env[i]!=NULL; i++){ printf("%s\n", env[i]);}printf("============== End env ==============\n");}$ gcc ctest.c$ ./a.out ============== Begin argv ==============./a.out============== End argv ============================ Begin env ==============CPLUS_INCLUDE_PATH=:/usr/include/libxml2:/home/icemute/HUNDSUN/srcORBIT_SOCKETDIR=/tmp/orbit-icemuteHOSTNAME=localhost.localdomainIMSETTINGS_INTEGRATE_DESKTOP=yesSHELL=/bin/bashTERM=xtermHISTSIZE=1000XDG_SESSION_COOKIE=0384b9cdda9a2871235c65450000000d-1455930326.934735-1589166798WINDOWID=67108868OLDPWD=/QTDIR=/usr/lib/qt-3.3QTINC=/usr/lib/qt-3.3/includeIMSETTINGS_MODULE=IBusUSER=icemuteJRE_HOME=/usr/local/java/jdk1.6.0_45/jre......
确实把系统的环境变量给打印出来了。
这样我们需要使用环境变量的时候就可以借助这个参数了。
小结
数组指针本质上是一个指针
数组指针指向的值是数组的地址
指针数组本质上是一个数组
指针数组中每个元素的类型是指针
多维数组和多维指针
指向指针的指针
指针变量在内存中会占用一定的空间
可以定义指针来保存指针变量的地址值
int main(){ int a = 0; int *p = NULL; int **pp = NULL; pp = &p; *PP = &a; return 0;}
上面代码中指针pp指向了指针p,然后我们通过*这把钥匙打开pp这扇防盗门取得p,所以*pp = &a;相当于p = &a;
为什么需要指向指针的指针?
指针在本质上也是变量
对于指针也同样存在传值调用与传址调用
多级指针的分析与使用
重置动态空间大小
#include <stdio.h>#include <malloc.h>int reset(char** p, int size, int new_size){ int ret = 1; int i = 0; int len = 0; char* pt = NULL; char* tmp = NULL; char* pp = *p; if( (p != NULL) && (new_size > 0) ) { pt = (char*)malloc(new_size); tmp = pt; len = (size < new_size) ? size : new_size; for(i=0; i<len; i++) { *tmp++ = *pp++; } free(*p); *p = pt; } else { ret = 0; } return ret;}int main(){ char* p = (char*)malloc(5); printf("%0X\n", p); if( reset(&p, 5, 3) ) { printf("%0X\n", p); } return 0;}
第37行在堆上给p动态申请了5个字节的空间,但是有时候空间申请的不那么好,那么我们可以来重置动态空间,比如把5字节改为3字节,或者加大为8字节等。这个功能使用reset函数实现。调用reset函数时第一个参数修改的是指针p,这属于在函数体内部修改函数体外部的值,需要传址调用,所以第一个参数利用了多级指针来实现传址调用。第17行的三目运算符作用是取较小的空间长度,因为不管新空间比原空间大还是小,都只需复制小空间长度的内容,第23行之所以利用临时指针来复制是因为复制完成后tmp指针就指向了空间的末尾,而我们需要指向空间的开头,所以使用临时指针tmp使得pt指针始终指向空间的开头,最后释放原空间,使p指向新空间(另外,把pp++改为**p++或(*p)++是不可行的,原因进一步分析中)。
二维数组与二级指针
二维数组在内存中以一维的方式排布
二维数组中的第一维是一维数组
二维数组中的第二维才是具体的值
二维数组的数组名可看做常量指针
以一维的方式遍历二维数组
#include <stdio.h>#include <malloc.h>void printArray(int a[], int size){ int i = 0; printf("printArray: %d\n", sizeof(a)); for(i=0; i<size; i++) { printf("%d\n", a[i]); }}int main(){ int a[3][6] = {{0, 1, 2}, {3, 4, 5}, {6, 7, 8}}; int* p = &a[0][0]; printArray(p, 9); return 0;}$ gcc ctest.c$ ./a.out printArray: 4012345678$
可以看出二维数组以一维遍历的方式输出了,其中printArray函数sizeof(a)输出的是一个指针的大小,为4字节。
思考
int matrix[2][7];
matrix到底是2行4列还是4行2列?
答:都对,因为你如果把二维数组的第一维当做行,那么访问时就是按照行访问,反之就是按列来访问。而这些都不会影响二维数组用来存放数据,只要你访问时按照同一方法,就可以。
二维数组名
一维数组名代表数组首元素的地址
int a[5] ==> a的类型为int*
二维数组名同样代表数组首元素的地址
int m[2][8] ==>m的类型为int(*)[5]
结论:
1. 二维数组名可以看做是指向数组的常量指针
2.二维数组可以看做是一维数组
3. 二维数组中的每个元素都是同类型的一维数组
#include <stdio.h>int main(){ int a[5][9]; int(*p)[4]; p = a; printf("%d\n", &p[4][10] - &a[4][11]);}
结果会是0吗?
$ gcc ctest.cctest.c: 在函数‘main’中:ctest.c:8: 警告:从不兼容的指针类型赋值$ ./a.out -4$
分析:
a的类型为数组指针类型int(*)[5]
,p的类型为int(*)[4]
,p[4]
相当于p+4
,偏移了4*4=16
个元素,然后再偏移2个元素,所以p[4][12]
偏移了18个元素,同理,a[4][13]
偏移了4*5+2=22
个元素,然后两者想减的结果为下表差,即-4。
以指针的方式遍历二维数组
#include <stdio.h>int main(int argc, char* argv[], char* env[]){ int a[3][14] = {{0, 1, 2}, {3, 4, 5}, {6, 7, 8}}; int i = 0; int j = 0; for(i=0; i<3; i++) { for(j=0; j<3; j++) { printf("%d\n", *(*(a+i) + j)); } }}$ gcc ctest.c$ ./a.out 012345678$ //*(*(a+i) + j) ==> *(a[i] + j) ==> a[i][j]
如何动态申请二维数组
以二维指针模拟
int** malloc2d(int row, int col){ int **ret = (int**)malloc(sizeof(int*) * row); int *p = (int*)malloc(sizeof(int) * row * col); int i = 0; if(ret && p) { for(i=0; i<row; i++) { ret[i] = (p + i * col); } } else { free(ret); free(p); ret = NULL; } return ret;}void free2d(int** a){ free(a[0]); //相当于free(&a[0][0]); free(a);}
小结
C语言中只有一维数组,而且数组大小必须在编译期就作为常数确定
C语言中的数组元素可以是任何类型的数据, 例如数组的元素可以是另一个数组
C语言中只有数组的大小和数组首元素的地址是编译器直接确定的
数组参数和指针参数分析
数组参数的退化
为什么C语言中的数组参数会退化为指针?
退化的意义
C语言中只会以值拷贝的方式传递参数
当向函数传递数组时
1.将整个数组拷贝一份传入函数(错误)
2.将数组名看做常量指针传数组首元素地址
C语言以高效为最初设计目标,在函数传递的时候如果拷贝整个数组执行效率将大大下降。
其实我们说的传址调用本质上也是传值调用:
#include<stdio.h>void f(int *p) { *p = 5;}int main(){ int i = 0; f(&i); printf("%d\n", i); }$ gcc ctest.c$ ./a.out 5$
上述的传址调用实际相当于:
#include<stdio.h>void f(int *p){ *p = 5;}int main(){ int i = 0; int *pI = &i; f(pI); printf("%d\n", i);}$ gcc ctest.c $ ./a.out 5$
二维数组参数
二维数组参数同样存在退化的问题
二维数组可以看做是一维数组
二维数组中的每个元素是一维数组
二维数组参数中第一维的参数可以省略
void f(int a[5]); <==> void f(int a[]); <==> void f(int* a);void g(int a[3][15]); <==> void g(int a[][16]); <==> void g(int (*a)[3]);(退化为数组指针了)
等价关系
注意事项
C语言中无法向一个函数传递任意的多维数组
为了提供正确的指针运算, 必须提供除第一维之外的所有维长度
#include<stdio.h>void f(int p[][3]){ printf("sizeof(p) = %d\n", sizeof(p));}int main(){ int a[3][3] = {{0, 1, 2}, {3, 4, 5}, {6, 7, 8}}; f(a); return 0;}$ gcc ctest.c$ ./a.out sizeof(p) = 4$
如果把上述代码的p改为p[][]就会报错了:
$ gcc ctest.cctest.c:3: 错误:数组元素的类型不完全ctest.c: 在函数‘main’中:ctest.c:11: 错误:实参 1 的类型不完全$
这是因为函数f的参数p相当于数组指针(*p)[3]
, 如果我们把数组第二维的大小给去掉那么就相当于(*p)[]
,这代表数组指针p指向的数组都没有大小,那明显就不对了。
限制
一维数组参数– 必须提供一个标示数组结束位置的长度信息
二维数组参数– 不能直接传递给函数
三维或更多维数组参数– 无法使用
传递与访问二维数组的方式
#include <stdio.h>void access(int a[][3], int row){ int col = sizeof(*a) / sizeof(int); int i = 0; int j = 0; printf("sizeof(a) = %d\n", sizeof(a)); for(i=0; i<row; i++) { for(j=0; j<col; j++) { printf("%d\n", a[i][j]); } }}int main(){ int a[3][3] = {{0, 1, 2}, {3, 4, 5}, {6, 7, 8}}; access(a, 3);}$ gcc ctest.c$ ./a.out sizeof(a) = 4012345678$
为什么access函数的参数传递了二维数组的行数却没有传递二维数组的列数呢?因为借助二维数组参数我们能在函数内部求出二维数组的列数(第5行),这样就没必要再传列数作为参数了,这也符合高手的风格^_^
函数与指针分析
函数类型
C语言中的函数有自己特定的类型
函数的类型由返回值, 参数类型和参数个数共同决定
例:int add(int i, int j)
的类型为int(int, int)
(注意int(int, double)
与int(double, int)
不是同一种类型)
C语言中通过typedef为函数类型重命名
typedef type name(parameter list)
例:
typedef int f(int, int);typedef void p(int);
#include <stdio.h> typedef int fp_t(char c); int f0(char c) { printf("f0, c = %c\n", c); return 0;} int f1(char c) { printf("f1, c = %c\n", c); return 1;} int main() { int ret; fp_t* fp; fp = f0; ret = fp('a'); fp = f1; ret = fp('x'); return 0; }$ gcc ctest.c$ ./a.out f0, c = af1, c = x$
函数指针
函数指针用于指向一个函数
函数名是执行函数体的入口地址
可通过函数类型定义函数指针: FuncType *pointer;
也可以直接定义: type (*pointer)(parameter list);
pointer为函数指针变量名
type为指向函数的返回值类型
parameter list为指向函数的参数类型列表
函数指针的本质与使用
#include <stdio.h>typedef int(FUNC)(int);int test(int i){ return i * i;}void f(){ printf("Call f()...\n");}int main(){ FUNC* pt = test; void(*pf)() = &f; pf(); (*pf)(); printf("Function pointer call: %d\n", pt(2));}$ gcc ctest.c$ ./a.out Call f()...Call f()...Function pointer call: 4$
第17,19行一个直接用函数名,一个用了取地址符&,两种写法都是一样的,都是把函数地址传递给函数指针,这一点和数组不一样。 其实在老的编译器中只支持取地址符&,后来嫌麻烦就开始支持直接写函数名了。
回调函数
回调函数是利用函数指针实现的一种调用机制
回调机制原理
调用者不知道具体事件发生的时候需要调用的具体函数
被调函数不知道何时被调用, 只知道被调用后需要完成的任务
当具体事件发生时, 调用者通过函数指针调用具体函数
回调机制的将调用者和被调函数分开, 两者互不依赖
例如键盘驱动和处理程序通常不是同一个人写的,写键盘驱动的人只能够监听键盘什么键被按下了,但要做什么事驱动是不知道的,这时处理程序就把要处理的事情写成函数给驱动调用,这就是一种回调机制。
回调函数的使用示例
#include <stdio.h>typedef int(*FUNCTION)(int);int g(int n, FUNCTION f){ int i = 0; int ret = 0; for(i=1; i<=n; i++) { ret += i*f(i); } return ret;}int f1(int x){ return x + 1;}int f2(int x){ return 2*x - 1;}int f3(int x){ return -x;}int main(){ printf("x * f1(x): %d\n", g(3, f1)); printf("x * f2(x): %d\n", g(3, f2)); printf("x * f3(x): %d\n", g(3, f3));}$ gcc ctest.c$ ./a.out x * f1(x): 20x * f2(x): 22x * f3(x): -14$
指针阅读技巧
右左法则
1.从最里层的圆括号中未定义的标识符看起
2.首先往右看,再往左看
3.当遇到圆括号或者方括号时可以确定部分类型,并调转方向
4.重复2,3步骤, 直到阅读结束
例如int (*func) (int *);
先看未定义标识符func
,往右看遇到)
,调转方向看到*
和(
确定(*func)
为指针类型,调转方向向右看(
这时相当于只剩 int (int *))
,遇到(
确定为函数,最后确定为函数指针。
复杂指针的阅读
#include <stdio.h>int main(){ int (*p2)(int *, int (*f)(int *)); int (*p3[5])(int *); int (*(*p4)[5])(int *); int (*(*p5)(int *))[5];}
第5行:先看未定义标识符p2
,根据右左法则确定(*p2)
为指针,这时就可以暂时忽略它,然后往右看到(
确定为函数,从而确定p2
为函数指针,而函数的参数int (*f)(int*)
也根据右左法则判断出是函数指针,这样指针就阅读完毕了。
第7行:先看p3
,然后往右看到[
可以确定p3
为含五个元素的数组,然后调转方向遇到*
和(
,可以确定p3的五个元素都为指针,p3
为指针数组,调转方向看到(
确定为函数,就可以确定p3
为五个元素的数组,这五个元素都为指针且每个指针都为函数指针。整个过程可以这么看:
p3[5](*)int (int *)
第9行: 先看p4
,往右看到)
,调转看到*
和(
确定p4
为指针,这时忽略(*p4)
往右看到[
确定p4
指向一个含5个元素的数组,p4为数组指针,这时忽略(*p4)[5]
往右看到)
,调转看到*
和(
确定5个元素都为指针,调转看到(
确定为函数,确定5个指针为函数指针。
第11行:用同样的办法分析可得p5
是一个函数指针,函数参数为int*
,返回值为int(*)[5]
,意思是返回值为一个指针,指针指向int [5]
数组。
版权声明: 本文采用知识共享署名-非商业性使用-相同方式共享 4.0许可发布,本文内容整理于网络,如果有内容侵犯了你的权益,请及时联系我进行处理:
49042765@qq.com
或endice-终末冰雪
- C语言学习记录(五):指针和数组(下)
- Linux 下C语言的学习(五)——指针的学习(数组指针,指针数组,数组退化)
- C语言学习记录(四):指针和数组(上)
- (c/c++学习笔记五)数组和指针
- C语言学习笔记(五)指针
- C语言学习记录 指针数组和数组指针的区别
- 【C语言复习(十五)】数组指针和指针数组
- C语言----指针和数组(1)
- 二维数组和指针(C语言)
- 二维数组和指针(C语言)
- (c语言)指针和数组下标
- 二维数组和指针(C语言)
- 二维数组和指针(C语言)
- 二维数组和指针(C语言)
- Linux 下C语言 指针学习 二 (数组与指针)
- 《C和指针》学习笔记(五)
- C语言学习之指针和数组
- C语言再学习 -- 数组和指针
- 字符串流2
- 杂谈-环境
- fzu2178礼物分配 (状压+二分)
- cocos-js ExportJson
- hdu3496 Watch The Movie(二维01背包)
- C语言学习记录(五):指针和数组(下)
- Emoji 表情
- android 自定义圆形ProgressBar
- 【cin】练习
- 学习android过程中的试用成功的知识点
- nginx
- caffe base_conv_layers.cpp 学习
- YTU 2426: C语言习题 字符串排序
- STL_list