目录
注意,数组初始化的时候,char str[20] = {0};是将数组初始化全部为'\0',int str[20]={0};是初始化全部为0。
用数组作为函数参数的话,实际上还是用数组指针作为函数参数,传递地址而不是内存拷贝会节省时间。
一、指针简介
我们将内存中字节的编号称为地址(Address)或指针(Pointer)。地址从 0 开始依次增加,对于 32 位环境,程序能够使用的内存为 4GB,最小的地址为 0,最大的地址为 0XFFFFFFFF。
下面的代码演示了如何输出一个地址:
#include <stdio.h>
int main(){
int a = 100;
char str[20] = "c.biancheng.net";
printf("%#X, %#X\n", &a, str);
return 0;
}
运行结果:
0X28FF3C, 0X28FF10%#X
表示以十六进制形式输出,并附带前缀0X
。a 是一个变量,用来存放整数,需要在前面加&
来获得它的地址;str 本身就表示字符串的首地址,不需要加&
。
C语言中有一个控制符%p
,专门用来以十六进制形式输出地址,不过 %p 的输出格式并不统一,有的编译器带0x
前缀,有的不带,所以此处我们并没有采用。
变量名和函数名只是地址的一种助记符,当源文件被编译和链接成可执行程序后,它们都会被替换成地址。
二、定义指针变量
datatype *name;
int a = 100;
int *p_a = &a;
//定义普通变量
float a = 99.5, b = 10.6;
char c = '@', d = '#';
//定义指针变量
float *p1 = &a;
char *p2 = &c;
//修改指针变量的值
p1 = &b;
p2 = &d;
定义指针变量时必须带*,给指针变量赋值时不能带*。
指针除了可以获取内存上的数据,也可以修改内存上的数据,例如:
#include <stdio.h>
int main(){
int a = 15, b = 99, c = 222;
int *p = &a; //定义指针变量
*p = b; //通过指针变量修改内存上的数据
c = *p; //通过指针变量获取内存上的数据
printf("%d, %d, %d, %d\n", a, b, c, *p);
return 0;
}
运行结果:
99, 99, 99, 99
*&a
可以理解为*(&a)
,&a
表示取变量 a 的地址(等价于 pa),*(&a)
表示取这个地址上的数据(等价于 *pa),
绕来绕去,又回到了原点,*&a
仍然等价于 a。
&*pa
可以理解为&(*pa)
,*pa
表示取得 pa 指向的数据(等价于 a),&(*pa)
表示数据的地址(等价于 &a),
所以&*pa
等价于 pa。
三、C语言数组指针详解
#include <stdio.h>
int main(){
int arr[] = { 99, 15, 100, 888, 252 };
int len = sizeof(arr) / sizeof(int); //求数组长度
int i;
for(i=0; i<len; i++){
printf("%d ", *(arr+i) ); //*(arr+i)等价于arr[i]
}
printf("\n");
return 0;
}
运行结果:
99 15 100 888 252
上面的arr是常量。数组指针是变量。
如果一个指针指向了数组,我们就称它为数组指针(Array Pointer)。
数组指针指向的是数组中的一个具体元素,而不是整个数组,所以数组指针的类型和数组元素的类型有关,上面的例子中,p 指向的数组元素是 int 类型,所以 p 的类型必须也是int *
。
反过来想,p 并不知道它指向的是一个数组,p 只知道它指向的是一个整数,究竟如何使用 p 取决于程序员的编码。
更改上面的代码,使用数组指针来遍历数组元素:
#include <stdio.h>
int main(){
int arr[] = { 99, 15, 100, 888, 252 };
int i, *p = arr, len = sizeof(arr) / sizeof(int);
for(i=0; i<len; i++){
printf("%d ", *(p+i) );
}
printf("\n");
return 0;
}
因为 p 只是一个指向 int 类型的指针,编译器并不知道它指向的到底是一个整数还是一系列整数(数组),
所以 sizeof(p) 求得的是 p 这个指针变量本身所占用的字节数,而不是整个数组占用的字节数。
下面是上面p的解释思想体现。
#include <stdio.h>
int main(){
int arr[] = { 99, 15, 100, 888, 252 };
int *p = &arr[2]; //也可以写作 int *p = arr + 2;
printf("%d, %d, %d, %d, %d\n", *(p-2), *(p-1), *p, *(p+1), *(p+2) );
return 0;
}
划重点:
更改上面的代码,借助自增运算符来遍历数组元素:
#include <stdio.h>
int main(){
int arr[] = { 99, 15, 100, 888, 252 };
int i, *p = arr, len = sizeof(arr) / sizeof(int);
for(i=0; i<len; i++){
printf("%d ", *p++ );
}
printf("\n");
return 0;
}
运行结果:
99 15 100 888 252
第 8 行代码中,*p++ 应该理解为 *(p++),每次循环都会改变 p 的值(p++ 使得 p 自身的值增加),以使 p 指向下一个数组元素。该语句不能写为 *arr++,因为 arr 是常量,而 arr++ 会改变它的值,这显然是错误的。
自加优先级大于*优先级大于+优先级。
四、C语言字符数组和字符串常量指针详解
字符数组归根结底还是一个数组,上节讲到的关于指针和数组的规则同样也适用于字符数组。更改上面的代码,使用指针的方式来输出字符串:
#include <stdio.h>
#include <string.h>
int main(){
char str[] = "http://c.biancheng.net";
char *pstr = str;
int len = strlen(str), i;
//使用*(pstr+i)
for(i=0; i<len; i++){
printf("%c", *(pstr+i));
}
printf("\n");
//使用pstr[i]
for(i=0; i<len; i++){
printf("%c", pstr[i]);
}
printf("\n");
//使用*(str+i)
for(i=0; i<len; i++){
printf("%c", *(str+i));
}
printf("\n");
return 0;
}
运行结果:
http://c.biancheng.net
http://c.biancheng.net
http://c.biancheng.net
除了字符数组,C语言还支持另外一种表示字符串的方法,就是直接使用一个指针指向字符串,例如:
char *str = "http://c.biancheng.net";
或者:
char *str;
str = "http://c.biancheng.net";
字符串中的所有字符在内存中是连续排列的,str 指向的是字符串的第 0 个字符;我们通常将第 0 个字符的地址称为字符串的首地址。字符串中每个字符的类型都是char,所以 str 的类型也必须是char *。
两种表示方法最根本的区别是在内存中的存储区域不一样。
字符数组存储在全局数据区或栈区,第二种形式的字符串存储在常量区。
全局数据区和栈区的字符串(也包括其他数据)有读取和写入的权限,
而常量区的字符串(也包括其他数据)只有读取权限,没有写入权限。
用代码解释一下:
#include <stdio.h>
int main(){
char *str = "Hello World!";
str = "I love C!"; //正确
str[3] = 'P'; //错误
return 0;
}
这段代码能够正常编译和链接,但在运行时会出现段错误(Segment Fault)或者写入位置错误。
第4行代码是正确的,可以更改指针变量本身的指向;
第5行代码是错误的,不能修改字符串中的字符。
最好应用字符数组表示字符串,不要用字符串常量表示。
五、C语言字符串数组理解小检测
#include <stdio.h>
int main(){
char str[20] = "c.biancheng.net";
char *s1 = str;
char *s2 = str+2;
char c1 = str[4];
char c2 = *str;
char c3 = *(str+4);
char c4 = *str+2;
char c5 = (str+1)[5];
int num1 = *str+2;
long num2 = (long)str;
long num3 = (long)(str+2);
printf(" s1 = %s\n", s1);
printf(" s2 = %s\n", s2);
printf(" c1 = %c\n", c1);
printf("num1 = %d\n", num1);
printf("num2 = %ld\n", num2);
printf("num3 = %ld\n", num3);
return 0;
}
运行结果:
s1 = c.biancheng.net
s2 = biancheng.net
c1 = a
c2 = c
c3 = a
c4 = e
c5 = c
num1 = 101
划重点:
num2 = 2686736
num3 = 2686738
//要是Int类型就不是+2了,是加8。
注意,数组初始化的时候,char str[20] = {0};是将数组初始化全部为'\0',int str[20]={0};是初始化全部为0。
用数组作为函数参数的话,实际上还是用数组指针作为函数参数,传递地址而不是内存拷贝会节省时间。
六、C语言指针作为返回值
总结而言,函数的内存被主函数弃用,但不是清空和不被其他线程应用覆盖。
七、C语言二级指针(指向指针的指针)详解,直接看最下面的图
八、C语言空指针NULL以及void指针
8.1 NULL指针
C语言没有一种机制来保证指向的内存的正确性。
记住下面代码是错误的,因为它没有初始化指针指向。
#include <stdio.h>
int main(){
char *str;
gets(str);
printf("%s\n", str);
return 0;
}
强烈建议对没有初始化的指针赋值为 NULL,例如:
char *str = NULL;
很多库函数都对传入的指针做了判断,如果是空指针就不做任何操作,或者给出提示信息。
- gets() 不会让用户输入字符串,也不会向指针指向的内存中写入数据;
- printf() 不会读取指针指向的内容,只是简单地给出提示,让程序员意识到使用了一个空指针。
从整体上来看,NULL 指向了地址为 0 的内存,而不是前面说的不指向任何数据。
在进程的虚拟地址空间中,最低地址处有一段内存区域被称为保留区,这个区域不存储有效数据,也不能被用户程序访问,将 NULL 指向这块区域很容易检测到违规指针。
关于虚拟地址空间的概念以及程序的内存分布,我们将在《C语言内存精讲》专题中深入讲解,现在读者只需要记住,在大多数操作系统中,极小的地址通常不保存数据,也不允许程序访问,NULL 可以指向这段地址区间中的任何一个地址。
8.2 void指针
void 用在函数定义中可以表示函数没有返回值或者没有形式参数,用在这里表示指针指向的数据的类型是未知的。
void *
表示一个有效指针,它确实指向实实在在的数据,
只是数据的类型尚未确定,在后续使用过程中一般要进行强制类型转换。
C语言动态内存分配函数 malloc() 的返回值就是void *
类型,在使用时要进行强制类型转换,请看下面的例子:
#include <stdio.h>
int main(){
//分配可以保存30个字符的内存,并把返回的指针转换为 char *
char *str = (char *)malloc(sizeof(char) * 30);
gets(str);
printf("%s\n", str);
return 0;
}
运行结果:
c.biancheng.net↙
c.biancheng.net
九、C语言指针数组(数组每个元素都是指针)详解
如果一个数组中的所有元素保存的都是指针,那么我们就称它为指针数组。代码如下:
#include <stdio.h>
int main(){
int a = 16, b = 932, c = 100;
//定义一个指针数组
int *arr[3] = {&a, &b, &c};//也可以不指定长度,直接写作 int *arr[]
//定义一个指向指针数组的指针
int **parr = arr;
printf("%d, %d, %d\n", *arr[0], *arr[1], *arr[2]);
printf("%d, %d, %d\n", **(parr+0), **(parr+1), **(parr+2));
return 0;
}
运行结果:
16, 932, 100
16, 932, 100
9.1 字符串指针数组
指针数组还可以和字符串数组结合使用,请看下面的例子:
#include <stdio.h>
int main(){
char *str[3] = {
"c.biancheng.net",
"C语言中文网",
"C Language"
};
printf("%s\n%s\n%s\n", str[0], str[1], str[2]);
return 0;
}
运行结果:
c.biancheng.net
C语言中文网
C Language
字符数组 str 中存放的是字符串的首地址,不是字符串本身,字符串本身位于其他的内存区域,和字符数组是分开的。
理解一下:
#include <stdio.h>
int main(){
char *str0 = "c.biancheng.net";
char *str1 = "C语言中文网";
char *str2 = "C Language";
char *str[3] = {str0, str1, str2};
printf("%s\n%s\n%s\n", str[0], str[1], str[2]);
return 0;
}
9.2 玩转指针数组和二级指针
#include <stdio.h>
int main(){
char *lines[5] = {
"COSC1283/1284",
"Programming",
"Techniques",
"is",
"great fun"
};
char *str1 = lines[1];
char *str2 = *(lines + 3);
char c1 = *(*(lines + 4) + 6);
char c2 = (*lines + 5)[5];
char c3 = *lines[0] + 2;
printf("str1 = %s\n", str1);
printf("str2 = %s\n", str2);
printf(" c1 = %c\n", c1);
printf(" c2 = %c\n", c2);
printf(" c3 = %c\n", c3);
return 0;
}
运行结果:
str1 = Programming
str2 = is
c1 = f
c2 = 2
c3 = E
char *lines[5]
定义了一个指针数组,它的每个元素的类型都是char *
。
在表达式中使用 lines 时,它会转换为一个类型为char **
的指针,lines 是二级指针,*(lines+i) 是一级指针,**(lines+i) 才是具体的字符。
还不理解,就去看:http://c.biancheng.net/view/vip_2021.html
十、C语言(二维)数组指针(指向二维数组的指针)详解
10.1 数组指针,针对二维
int a[3][4] = { {0, 1, 2, 3}, {4, 5, 6, 7}, {8, 9, 10, 11} };
假设数组 a 中第 0 个元素的地址为 1000,那么每个一维数组的首地址如下图所示:
为了更好的理解指针和二维数组的关系,我们先来定义一个指向 a 的指针变量 p:
int (*p)[4] = a;
括号中的*
表明 p 是一个指针,它指向一个数组,数组的类型为int [4]
,这正是 a 所包含的每个一维数组的类型。
[ ]
的优先级高于*
,( )
是必须要加的,如果赤裸裸地写作int *p[4]
,那么应该理解为int *(p[4])
,p 就成了一个指针数组,而不是二维数指针。
代码如下:
#include <stdio.h>
int main(){
int a[3][4] = { {0, 1, 2, 3}, {4, 5, 6, 7}, {8, 9, 10, 11} };
int (*p)[4] = a;
printf("%d\n", sizeof(*(p+1)));
return 0;
}
运行结果:
16
可以很容易推出以下的等价关系:
a+i == p+i
a[i] == p[i] == *(a+i) == *(p+i)
a[i][j] == p[i][j] == *(a[i]+j) == *(p[i]+j) == *(*(a+i)+j) == *(*(p+i)+j)
指针数组和二维数组指针有着本质上的区别:指针数组是一个数组,只是每个元素保存的都是指针,以上面的 p1 为例,在32位环境下它占用 4×5 = 20 个字节的内存。二维数组指针是一个指针,它指向一个二维数组,以上面的 p2 为例,它占用 4 个字节的内存。
十一、函数指针
一个函数总是占用一段连续的内存区域,函数名在表达式中有时也会被转换为该函数所在内存区域的首地址,这和数组名非常类似。我们可以把函数的这个首地址(或称入口地址)赋予一个指针变量,使指针变量指向函数所在的内存区域,然后通过指针变量就可以找到并调用该函数。这种指针就是函数指针。
它的定义方式的思想和数组指针很像。
函数指针的定义形式为:
returnType (*pointerName)(param list);
returnType 为函数返回值类型,pointerNmae 为指针名称,param list 为函数参数列表。
参数列表中可以同时给出参数的类型和名称,也可以只给出参数的类型,省略参数的名称,这一点和函数原型非常类似。
注意( )的优先级高于*,第一个括号不能省略。
如果写作returnType *pointerName(param list);
就成了函数原型,它表明函数的返回值类型为returnType *。
#include <stdio.h>
//返回两个数中较大的一个
int max(int a, int b){
return a>b ? a : b;
}
int main(){
int x, y, maxval;
//定义函数指针
int (*pmax)(int, int) = max; //也可以写作int (*pmax)(int a, int b)
printf("Input two numbers:");
scanf("%d %d", &x, &y);
maxval = (*pmax)(x, y);
printf("Max value: %d\n", maxval);
return 0;
}
运行结果:
Input two numbers:10 50↙
Max value: 50
十二、挑战一下最复杂的指针模式
http://c.biancheng.net/view/vip_2024.html
十三、main()函数的高级用法:接收用户输入的数据
#include <stdio.h>
int main(int argc, char *argv[]){
int i;
printf("The program receives %d parameters:\n", argc);
for(i=0; i<argc; i++){
printf("%s\n", argv[i]);
}
return 0;
}
将生成后的程序放在D:\demo
目录下,命名为main.exe
,打开 cmd(命令提示符程序),输入D:\demo\main.exe C语言中文网 c.biancheng.net C-Lang
,程序的运行结果如下: