zoukankan      html  css  js  c++  java
  • 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
    ", p1, *p1);
    	printf("address of c is %#x, value of c is %c
    ", 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
    ", *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
    ", *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
    ", 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
    ", 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
    "; // C 语言中字符串不可修改
    	char str2 [] = {"hello
    "};// 数组可以修改
    
    	//str1[0] = 'a'; // str1 修改会导致 segmentation fault
    	str2[0] = 'a';
    	printf("%s
    ", str2);
    }
    

    上面代码中,字符串是不可修改的,所以可以用 const 限制,如果有代码则会在编译时报错:

    int main()
    {
    	char * str1 = "hello
    "; // C 语言中字符串不可修改
    	const char str2 [] = {"hello
    "};// 数组可以修改
    
    	str1[0] = 'a'; // str1 修改会导致 segmentation fault
    	str2[0] = 'a';
    	printf("%s
    ", 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
    ", 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
    "};
    	int *s = p;
    	printf("%c, %c, %c, %c, %#x
    ", *p, *(p+1), *(p+2), *(p+3), *s);
    }
    

    []

    在数组中,保存的是相同类型的元素。通过下标可以访问到每一个元素,不需要我们在编程的时候关系元素占几个字节。这跟指针的加减运算是一样的。p[0] 等价于 *p,p[1] 等价于 *(p+1),以此类推:

    #include <stdio.h>
    
    int main()
    {
    	const char *p = {"hello
    "};
    	printf("%c, %c, %c, %c
    ", *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
    ", i, argv[i]);
    	}
    	
    	i = 0;
    	while(argv[i] != NULL) {
    		printf("argv[%d] is: %s
    ", 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
    "};
    	char * p = {"hello
    "};
    	printf("a is: %s
    ", a);
    	printf("p is: %s
    ", p);
    	//a = "hello";			// a 是标签,数组不可变,否则编译报错
    	p = "world
    ";			// p 是指针,可以变
    	printf("a is: %s
    ", a);
    	printf("p is: %s
    ", p);
    }
    

    字符空间和非字符空间

    关于char、unsigned char 和 signed char 三种类型直接的差别,可以参考:http://bbs.chinaunix.net/thread-889260-1-1.html

    内存中的数据空间可以分为两类:

    • 字符空间:存储的数据是可读的字符串,以 结束。用 char 来表示,例如 char a[10];。用 strcpy 复制数据,复制时以 结束,或者用 strncpy 复制。
    • 非字符空间:存储的是二进制数据,不可读。用 unsigned char 来表示,例如 unsigned char b[10]。用 memcpy 复制数据,复制时需要指定字节个数
    int buf[10];
    int source[1000];
    
    memcpy(buf, source, 10*sizeof(int));
    

    数组的初始化

    注意:C 语言中只有字符串常量。因为 C 语言没有字符串变量的概念,如果想修改字符串的值,必须将字符串存储为字符数组。所有字符串都以 结尾。

    • 声明数组时,同时赋值一个内存空间:
      C 语言本身不支持空间赋值,通常是编译器自动对这种赋值转换为逐个元素赋值,可以反汇编查看一下。
    char a[] = "hello
    "; // C 编译器看到双引号时,自动在末尾加 
    char b[10] = {'h', 'e', 'l', 'l', 'o', '
    ', ''}; // 未赋值的元素默认是0
    char c[] = {"hello
    "}; // 因为双引号和大括号都用来划分存储空间,可省略大括号
    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
    ", a[5]);
    	printf("%d
    ", b[1][1]);
    	printf("%d
    ", p3[1][1]);
    }
    

    对于三维数组 int a[2][3][4];,可以用指针表示:

    int (*p) [3][4];
    
  • 相关阅读:
    js遍历不要使用index 而是使用id(数据唯一表示)
    eureka
    Mybatis-plus自动填充字段的值(如createTime,UpdateTime)
    计算机网络入门
    操作系统入门
    计算机组成原理入门
    《事实》读书笔记
    推荐算法入门
    源码编译安装apache2.4脚本
    Mycat实现读写分离
  • 原文地址:https://www.cnblogs.com/kika/p/10851509.html
Copyright © 2011-2022 走看看