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];
C语言的指针和数组的更多相关文章
- C语言中指针和数组
C语言数组与指针的那些事儿 在C语言中,要说到哪一部分最难搞,首当其冲就是指针,指针永远是个让人又爱又恨的东西,用好了可以事半功倍,用不好,就会有改不完的bug和通不完的宵.但是程序员一般都有一种迷之 ...
- C语言之指针与数组总结
和指针相关的问题口诀1: 1. 地址变量得地址,得谁地址指向谁 和指针相关的问题要画图: 内容变量画房子,指针画箭头 ---->口 ------------------------------- ...
- c语言,指针与数组--指针与二维数组2
指向一维数组的指针 char (*p)[10] ;指向一维数组的指针类型 typedef char(*TYPE_P2ARRAY)[10] ; 该指针可以指向数组 ,且使用起来效果节本相同, ...
- C语言中指针和数组的区别
看<C专家编程>一书,看到数组与指针并不相同一章,遂做了一段测试: 代码: #include <stdio.h> #include <stdlib.h> int m ...
- C语言中 指针和数组
C语言的数组表示一段连续的内存空间,用来存储多个特定类型的对象.与之相反,指针用来存储单个内存地址.数组和指针不是同一种结构因此不可以互相转换.而数组变量指向了数组的第一个元素的内存地址. 一个数组变 ...
- C语言使用指针表示数组的注意事项
1)数组名是指针常量 如对指针变量可以进行++运算,但是对数组名却不允许,另外,对数组名的赋值运算也是错误的 2)注意指针变量的当前值 指针变量的值在程序运行过程中可能经常改变,要对此注意 3)数组越 ...
- Android JNI编程(四)——C语言多级指针、数组取值、从控制台输入数组
版权声明:本文出自阿钟的博客,转载请注明出处:http://blog.csdn.net/a_zhon/. 目录(?)[+] 一:前面我们介绍了一级指针的相关概念和用发,今天我们就来说一说多级指针. 1 ...
- C语言_指针和数组的几种访问形式
敲几行代码来看看几种访问的形式~ #include <stdio.h>;int main() { ] = {, , , , , }; //初始化5个元素的一维数组 int *p = arr ...
- 浅谈C语言 extern 指针与数组
/* * d.c * * Created on: Nov 15, 2011 * Author: root */ #include "apue.h" int a[] = {3,2}; ...
随机推荐
- linux centos中安装flash player
本机为centos 7.0 64 1.用火狐浏览器随便打开一个视频网站,找到一个视频点进去,网站会提示未安装flash player.点击进去到adobe官网.2.下载flash player插件,可 ...
- Linux下单机部署ELK日志收集、分析环境
一.ELK简介 ELK是elastic 公司旗下三款产品ElasticSearch .Logstash .Kibana的首字母组合,主要用于日志收集.分析与报表展示. ELK Stack包含:Elas ...
- [工具] BurpSuite--XssValidator插件
0x00 安装 所需软件: 1.burpsuite 2.xssvalidator 源码:https://github.com/nVisium/xssValidator(按照编译指导编译) burpsu ...
- 什么是lease机制?
分布式系统理论之租约机制学习 一,租约机制介绍 在分布式系统中,往往会有一个中心服务器节点.该节点负责存储.维护系统中的元数据.如果系统中的各种操作都依赖于中心服务器上的元数据,那么中心服务器很容易成 ...
- 3D绘图计算器(geogebra[5.0.385.0])使用QQ浏览器打开下载
点击这里下载3D绘图计算器
- InnoDB数据库 ibdata1 被删除后 的恢复方法
前提条件:1 ibdata1 被删除 2 数据库文件还存在 特别是 ibd文件 3 原来数据库表结构及索引还在 恢复步骤: 1. 将原来的数据文件COPY到其它目录下. 2. 创建同名表,表结 ...
- js-展开评论与隐藏评论
//控制展开评论和隐藏评论 controldiscuss(){ $(".opendiss").click(function(){ if($(this).context.innerH ...
- python 习题
文件内容为一个多层元组,遍历该元组,当全为数字时输出数字之和,全为字母输出字符串,有数字有字母输出False,并将该内容写入到该文件的下一行中 # 方法一: t1= ((1,2,3),("a ...
- anaconda 安装caffe,cntk,theano-未整理
一,anancona 安装 https://repo.anaconda.com/archive/ conda create -n caffe_gpu -c defaults python=3.6 ca ...
- linux 内存
[转]Linux 查看内存(free buffer cache) 转自:http://elf8848.iteye.com/blog/1995638 Linux下如何查内存信息,如内存总量.已使用量.可 ...