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];

posted on 2019-01-20 22:15  kikajack  阅读(394)  评论(0编辑  收藏  举报