指针和内存

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

例如,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. Codeforces Round #575 (Div. 3) C. Robot Breakout (模拟,实现)

    C. Robot Breakout time limit per test3 seconds memory limit per test256 megabytes inputstandard inpu ...

  2. Stanford CS229 Machine Learning by Andrew Ng

    CS229 Machine Learning Stanford Course by Andrew Ng Course material, problem set Matlab code written ...

  3. CreateRemoteThread

    CreateRemoteThread是一个Windows API函数,它能够创建一个在其它进程地址空间中运行的线程(也称:创建远程线程)..

  4. MyBatis:Parameter Maps collection does not contain value for 的问题解决

    Result Maps collection does not contain value for   frontpreviewprofitManage.cdata 出现上述错误 主要是因为你的sel ...

  5. CSS中filter属性的使用

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

  6. 软件的三大类型-单机类型、BS类型、CS类型

    单机类型:最开始的软件就是那些不需要联网的单机软件. CS类型:有的程序需要统一管理软件中使用的数据, 所以就将保存数据的数据库统一存放在一台主机中, 所有的用户在需要数据时都要从主机获取, 这时就分 ...

  7. NOIP2016提高A组五校联考4总结

    坑爹的第一题,我居然想了足足3个小时,而且还不确定是否正确. 于是,我就在这种情况下心惊胆跳的打了,好在ac了,否则就爆零了. 第二题,树形dp,本来差点就想到了正解,结果时间不够,没打完. 第三题, ...

  8. Python 函数Ⅱ

    以上代码中,[1,2,3] 是 List 类型,"Runoob" 是 String 类型,而变量 a 是没有类型,她仅仅是一个对象的引用(一个指针),可以是 List 类型对象,也 ...

  9. ForkJoinPool及并行流解析

    parallelStream原理. parallelStream是并行流,依赖jdk1.7出现的Fork/Join框架. Fork/Join框架的核心是工作窃取(work-stealing)算法.那么 ...

  10. 如何查看运行的docker container 的 执行 docker run的命令

    前言 就是我备份了一下 mysql_container, 然后我想启用 新的备份的mysql_container 但是之前的docker run image xxxxxx这些都已经忘记了 我想找一下之 ...