C语言的指针和数组
指针和内存
指针变量也是个变量,不过保存的是另一个变量的地址。另外编译器还会记住指针所指向变量的类型,从而在指针运算时根据变量类型采取不同操作。
例如,char * a
定义了char 类型的指针变量 a,通过 *a
读取数据时,每次只会读一个字节(char 类型变量的长度)。而int * i
定义了 int 类型的指针变量 i,通过 *i
读取数据时,每次会读两个或四个字节(int 类型变量的长度跟编译器平台有关)。
#include <stdio.h>
int main()
{
int a = 666;
char c = 'a';
int * p1 = &a; // 相当于(int *) p1,表示 p1 是执行 int 类型的指针
char * p2; // 相当于(char *) p2,表示 p2 是执行 char 类型的指针
p2 = &c; // & 符号用于取变量的地址
printf("address of a is %#x, value of a is %d\n", p1, *p1);
printf("address of c is %#x, value of c is %c\n", p2, *p2);
}
输出:
address of a is 0x107900bc, value of a is 666
address of c is 0x107900bb, value of c is a
char 型指针显示多个字节的问题
#include <stdio.h>
int main()
{
float a = 1.2;
char * p = (char *)&a; // 这里的 p 指向有符号字符型变量,符号位为1时会打印4个字节
printf("%#x\n", *p);
}
输出:
0xffffff9a
要解决这个问题,把上面的 char * p = &a;
变成 unsigned char * p = &a;
即可。
指针类型转换
有时我们需要用 char * 按照字节大小读取数据,但是非 char 类型的指针当做 char 指针处理时会报错或警告。这时需要强制类型转换:
#include <stdio.h>
int main()
{
int a = 0x77777777;
char * p = (char *)&a;
printf("%#x\n", *p);
}
段错误
指针操作如有不慎,会经常看到 Segmentation Fault 段错误。这是因为指针指向了非法的内存,例如下面的代码执行的内存地址,操作系统是不允许访问的:
#include <stdio.h>
int main()
{
int a = 0x12345678;
int * p = &a;
p = 0x00000001;
printf("address of a is %#x, value of a is %d\n", p1, *p1);
}
内存除了用于存放程序运行时的数据外,还有一部分内存用于操作硬件。例如内存的某一段连续空间用于映射显存、I2C、USB 设备等。
大端存储、小端存储
对于单字节的 char 类型变量不存在这个问题。但多字节的变量,高字节存储在内存的高地址还是低地址,决定了采用哪种存储方式。
- 大端模式 Big-Endian:低地址存放高位,类似于把数据当作字符串顺序处理:地址由小向大增加,而数据从高位往低位放;和阅读习惯一致。
- 小端模式 Little-Endian:低地址存放低位,将地址的高低和数据位权有效地结合起来,高地址部分权值高,低地址部分权值低。
#include <stdio.h>
int main()
{
int a = 0x12345678;
unsigned char * p1 = &a;
printf("address of a is %#x, value of a is %#x\n", p1, *p1);
}
上面代码在 32 位平台上运行时,通过 char 类型的指针只读取第一个字节,如果输出 78 表示是小端存储(低地址存放低位),否则是大端存储。输出:
address of a is 0xbb483e84, value of a is 0x78
目前Intel的80x86系列芯片是唯一还在坚持使用小端的芯片,ARM芯片默认采用小端,但可以切换为大端。另外,对于大小端的处理也和编译器的实现有关,在C语言中,默认是小端(但在一些对于单片机的实现中却是基于大端,比如Keil 51C),Java是平台无关的,默认是大端。在网络上传输数据普遍采用的都是大端。
指针的修饰符
const
C 语言的 const 比较弱,很容易绕过去,例如通过指针。const 修饰的变量仍然存储在读写区,而非只读区。
- 指针可以指向任意变量,但是不可通过指针修改变量值。两种写法:
const char * p; // 推荐使用,相当于 const ((char *) p)
char const * p; // 不推荐
- 指针只能指向指定的变量,但是变量值可以任意修改。两种写法:
char * const p; // 推荐使用,相当于 (char *) (const p)
char * p const; // 不推荐
- 指针只能指向指定的变量,且不可通过指针修改变量值
const char * const p; // 相当于 const ((char *) (const p))
综合示例:
#include <stdio.h>
int main()
{
char * str1 = "hello\n"; // C 语言中字符串不可修改
char str2 [] = {"hello\n"};// 数组可以修改
//str1[0] = 'a'; // str1 修改会导致 segmentation fault
str2[0] = 'a';
printf("%s\n", str2);
}
上面代码中,字符串是不可修改的,所以可以用 const 限制,如果有代码则会在编译时报错:
int main()
{
char * str1 = "hello\n"; // C 语言中字符串不可修改
const char str2 [] = {"hello\n"};// 数组可以修改
str1[0] = 'a'; // str1 修改会导致 segmentation fault
str2[0] = 'a';
printf("%s\n", str2);
}
编译报错:
/code/main.c: In function ‘main’:
/code/main.c:9:2: error: assignment of read-only location ‘str2[0]’
str2[0] = 'a';
const 变量的绕过(越界)
#include <stdio.h>
int main()
{
int a = 0x66667777;
int b = 0x11111111;
int *p = &b;
*(p+1) = 0xffffffff;
printf("%#x\n", a);
}
volatile
编译器默认的优化是开启的。但有时候我们操作的内存是映射到硬件的,此时可能需要关闭优化。
volatile char * p = 0x20;
while (*p == 0x20) ...
typedef
指针可以指向任意类型的资源,例如 int、char、数组、函数。指定简明易读的别名可以提高代码可读性。
char * name_t;
typedef char * name_t;
name_t myVar;
指针的运算符
++、–、+、-
指针的加减操作,跟指针指向变量的具体类型有关。指针指向的变量占几个字节,指针每次加减一就是加减几个字节,确保刚好可以指向下一个同类型元素。
#include <stdio.h>
int main()
{
const char *p = {"hello\n"};
int *s = p;
printf("%c, %c, %c, %c, %#x\n", *p, *(p+1), *(p+2), *(p+3), *s);
}
[]
在数组中,保存的是相同类型的元素。通过下标可以访问到每一个元素,不需要我们在编程的时候关系元素占几个字节。这跟指针的加减运算是一样的。p[0] 等价于 *p,p[1] 等价于 *(p+1),以此类推:
#include <stdio.h>
int main()
{
const char *p = {"hello\n"};
printf("%c, %c, %c, %c\n", *p, p[0], *(p+1), p[1]);
}
指针的逻辑运算
指针可以进行比较,>= 、<= 、== 、!= 四种。
- 跟特殊值 0x0 或 NULL 这个无效地址进行比较,相等则表示结束。
- 必须是同类型的指针,比较时才有意义。
多级指针
常用的是二维指针,二维以上基本上不用。
当在内存中有多个离散的变量时,为了放在一个变量中统一访问,就需要把这个用作访问入口的统一变量设计为数组,数组中的每个元素都是指针,执行原始变量。
语法的简单示例:
int 变量int a;
← int 变量的指针int * p = &a;
← int 变量的指针的指针int **p2 = &p;
bash 终端可以在命令后面带参数,编译器会把所有参数汇总到 main 函数的参数中:
#include <stdio.h>
int main(int argc, char ** argv)
{
int i;
for (i = 0; i < argc; i++) {
printf("argv[%d] is: %s\n", i, argv[i]);
}
i = 0;
while(argv[i] != NULL) {
printf("argv[%d] is: %s\n", i, argv[i++]);
}
return 0;
}
# ./build 666 hello world !
argv[0] is: ./build
argv[1] is: 666
argv[2] is: hello
argv[3] is: world
argv[4] is: !
argv[1] is: ./build
argv[2] is: 666
argv[3] is: hello
argv[4] is: world
argv[5] is: !
数组
数组的内存操作
数组是地址操作的一种形式,使用的时候跟指针几乎一样。通过数组分配的内存空间的特性如下:
- 大小:在定义的时候指定,可以通过 malloc 分配,也可以通过元素的类型及个数 int[10] 这种形式分配
- 读取方式:通过数组中的元素类型确定。例如 char 类型的数组,每次读取 1 个字节
int a[10]; // 分配 4*10Byte 的内存,a 是指向这个内存的标签,不可变,不是指针
C 语言只有指针的概念,并没有真正意义的数组,所以在用指针操作数组时,需要注意:不要越界。
#include <stdio.h>
int main(int argc, char ** argv)
{
char a[] = {"hello\n"};
char * p = {"hello\n"};
printf("a is: %s\n", a);
printf("p is: %s\n", p);
//a = "hello"; // a 是标签,数组不可变,否则编译报错
p = "world\n"; // p 是指针,可以变
printf("a is: %s\n", a);
printf("p is: %s\n", p);
}
字符空间和非字符空间
关于char、unsigned char 和 signed char 三种类型直接的差别,可以参考:http://bbs.chinaunix.net/thread-889260-1-1.html
内存中的数据空间可以分为两类:
- 字符空间:存储的数据是可读的字符串,以 \0 结束。用 char 来表示,例如
char a[10];
。用 strcpy 复制数据,复制时以 \0 结束,或者用 strncpy 复制。 - 非字符空间:存储的是二进制数据,不可读。用 unsigned char 来表示,例如
unsigned char b[10]
。用 memcpy 复制数据,复制时需要指定字节个数。
int buf[10];
int source[1000];
memcpy(buf, source, 10*sizeof(int));
数组的初始化
注意:C 语言中只有字符串常量。因为 C 语言没有字符串变量的概念,如果想修改字符串的值,必须将字符串存储为字符数组。所有字符串都以 \0 结尾。
- 声明数组时,同时赋值一个内存空间:
C 语言本身不支持空间赋值,通常是编译器自动对这种赋值转换为逐个元素赋值,可以反汇编查看一下。
char a[] = "hello\n"; // C 编译器看到双引号时,自动在末尾加 \0
char b[10] = {'h', 'e', 'l', 'l', 'o', '\n', '\0'}; // 未赋值的元素默认是0
char c[] = {"hello\n"}; // 因为双引号和大括号都用来划分存储空间,可省略大括号
int i[] = {12, 23, 666};
- 声明数组后,逐个元素赋值:
char a[10];
a[0] = 'h';
a[1] = 'e';
...
字符串数组和字符串指针的差异
字符串是 C 语言中需要特别注意的地方。字符串常量赋值到数组时,实际上会先创建一个数组变量,然后依次把每个字符拷贝到这个数组中,数组指向的变量跟字符串常量无关,可以修改。但字符串赋值到指针时,指针指向的就是这个字符串常量,此时指针指向的值不可修改。
char a[10] = {"hello"}; // 内存中分配了一个字符串常量空间和一个字符串变量空间,变量 a 指向这个变量空间,可以修改空间中的元素
a[2] = 'w'; // OK
char *p = "hello"; // 内存中只有一个字符串常量空间和一个指向该常量的指针变量,指针变量 p 指向常量,不可修改
p[2] = 'w'; // 报错 segmentation fault
数组名是个标签,不可赋值
C 语言中,数组中的每个元素可以修改,但是不可直接对数组名进行赋值。如果想再次赋值,只能逐个元素赋值。
int a[] = {2, 5, 6};
a = {3, 5}; // 编译报错,数组名类似函数名,是个常量标签,不可赋值
内存空间拷贝函数
内存空间逐一赋值操作很常见,所以 C 语言将其封装为字符串拷贝函数。可以在 Linux 下通过 man 3 strcpy
之类的命令查看函数定义。
strcpy 函数
strcpy 函数碰到 0 就停止拷贝。如果源字符串太长,strcpy 可能导致内存泄漏,一般不用。函数原型如下:
char *strcpy(char *dest, const char *src);
char a[] = "666";
strcpy(a, "hello world");
strncpy 函数
strncpy 函数可以限制拷贝的数量,防止发生越界。
char *strcpy(char *dest, const char *src, size_t n);
指针数组
数组中存在指针,构成指针数组。指针数组就是二级指针。
int *a[10]; // 开辟 10 个空间存放数组 a,a 中放 (int *) 类型的指针
int **a; // ((int *) *) a
将数组名保存为指针
C 语言中,一维数组的数组名变量中放的就是数组首元素的地址,可以直接赋值给指针,并用这个指针访问数组中的元素。但二维数组跟二维指针没有任何关系。
下面例子会报错,p2 指向指针数组,但 b 指向两个连续的内存块,每块内存由 5 个 int 类型变量组成
#include <stdio.h>
int main()
{
int a[10]; // a 是数组标签,表示一块由 10 个 int 元素组成的空间
int b[2][5]; // b 是数组标签,表示两块空间,各由 5 个 int 元素组成
int *p1 = a;
int **p2 = b; // 这一行会报错
int *p4 [5] = b; // 这一行会报错,这里 p4 是数组,其中的每一个元素都是 int 类型的指针
int (*p3)[5] = b; // 正常编译,这里 p3 是指针,指向一块由 5 个 int 元素组成的空间
printf("%d\n", a[5]);
printf("%d\n", b[1][1]);
printf("%d\n", p3[1][1]);
}
对于三维数组 int a[2][3][4];
,可以用指针表示:
int (*p) [3][4];