指针和内存

指针变量也是个变量,不过保存的是另一个变量的地址。另外编译器还会记住指针所指向变量的类型,从而在指针运算时根据变量类型采取不同操作。

例如,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];

C语言的指针和数组的更多相关文章

  1. C语言中指针和数组

    C语言数组与指针的那些事儿 在C语言中,要说到哪一部分最难搞,首当其冲就是指针,指针永远是个让人又爱又恨的东西,用好了可以事半功倍,用不好,就会有改不完的bug和通不完的宵.但是程序员一般都有一种迷之 ...

  2. C语言之指针与数组总结

    和指针相关的问题口诀1: 1. 地址变量得地址,得谁地址指向谁 和指针相关的问题要画图: 内容变量画房子,指针画箭头 ---->口 ------------------------------- ...

  3. c语言,指针与数组--指针与二维数组2

    指向一维数组的指针   char (*p)[10] ;指向一维数组的指针类型 typedef  char(*TYPE_P2ARRAY)[10]  ;   该指针可以指向数组 ,且使用起来效果节本相同, ...

  4. C语言中指针和数组的区别

    看<C专家编程>一书,看到数组与指针并不相同一章,遂做了一段测试: 代码: #include <stdio.h> #include <stdlib.h> int m ...

  5. C语言中 指针和数组

    C语言的数组表示一段连续的内存空间,用来存储多个特定类型的对象.与之相反,指针用来存储单个内存地址.数组和指针不是同一种结构因此不可以互相转换.而数组变量指向了数组的第一个元素的内存地址. 一个数组变 ...

  6. C语言使用指针表示数组的注意事项

    1)数组名是指针常量 如对指针变量可以进行++运算,但是对数组名却不允许,另外,对数组名的赋值运算也是错误的 2)注意指针变量的当前值 指针变量的值在程序运行过程中可能经常改变,要对此注意 3)数组越 ...

  7. Android JNI编程(四)——C语言多级指针、数组取值、从控制台输入数组

    版权声明:本文出自阿钟的博客,转载请注明出处:http://blog.csdn.net/a_zhon/. 目录(?)[+] 一:前面我们介绍了一级指针的相关概念和用发,今天我们就来说一说多级指针. 1 ...

  8. C语言_指针和数组的几种访问形式

    敲几行代码来看看几种访问的形式~ #include <stdio.h>;int main() { ] = {, , , , , }; //初始化5个元素的一维数组 int *p = arr ...

  9. 浅谈C语言 extern 指针与数组

    /* * d.c * * Created on: Nov 15, 2011 * Author: root */ #include "apue.h" int a[] = {3,2}; ...

随机推荐

  1. docker命令(随时补充)

    导入导出容器:https://blog.csdn.net/LEoe_/article/details/78685156 拷贝文件到容器内:docker cp 本地路径 容器长ID:容器路径

  2. 清北学堂提高组突破营游记day5

    长者zhx来啦.. (又要送冰红茶了...) zhx一上来就讲动态规划...是不是要逼死人.... 动态规划: 最简单的例子:斐波那契数列.因为他是递推(通项公式不算)的,所以前面的已经确定的项不会影 ...

  3. [易学易懂系列|rustlang语言|零基础|快速入门|(11)|Structs结构体]

    [易学易懂系列|rustlang语言|零基础|快速入门|(11)] 有意思的基础知识 Structs 我们今天来看看数据结构:structs. 简单来说,structs,就是用来封装相关数据的一种数据 ...

  4. python基础练习题7

    1.创建Person类,属性有姓名.年龄.性别,创建方法personInfo,打印这个人的信息2.创建Student类,继承Person类,属性有学院college,班级class,重写父类perso ...

  5. CSS中filter属性的使用

    filter 属性定义了元素的可视效果 blur 给图像设置高斯模糊."radius"一值设定高斯函数的标准差,或者是屏幕上以多少像素融在一起, 所以值越大越模糊. 如果没有设定值 ...

  6. CSS-overflow-scroll 滑动粘手

    长列表的滑动,CSS属性给了 overflow: auto:在IOS上可能会出现“粘手”效果,即滑动很慢.卡顿.粘手 解决: 启动硬件加速渲染: -webkit-overflow-scrolling: ...

  7. webview默认是不开启localstorage的

    .setDomStorageEnabled(true);// 打开本地缓存提供JS调用,至关重要 转载 https://blog.csdn.net/xhf_123/article/details/77 ...

  8. Keras class_weight和sample_weight用法

    搬运: https://stackoverflow.com/questions/57610804/when-is-the-timing-to-use-sample-weights-in-keras i ...

  9. MySQL报错:Cause: java.sql.SQLException: Incorrect string value: '\xE6\x9D\xA8","...' for column 'obj_value' at row 1

    1.插入MySQL表时,报错:Cause: java.sql.SQLException: Incorrect string value: '\xE6\x9D\xA8","...' ...

  10. 2019年7月25日 shell练习--PAT题目1006:换个格式输出整数(失败案例)

    让我们用字母 B 来表示“百”.字母 S 表示“十”,用 12...n 来表示不为零的个位数字 n(<),换个格式来输出任一个不超过 3 位的正整数.例如 234 应该被输出为 BBSSS123 ...