C 语言数组:不要再叫我是指针了!
在整理有关指针和数组名之间的隐式转换、二维数组、指针数组等笔记时,总感觉自己对指针和数组本身概念的理解比较模糊,因此特地整理此文,梳理思路。
指针和数组之间没有任何关系!指针就是指针,指针变量在 32 位系统下,永远占 4 个字节,其值为一个内存的地址。指针可以指向任何地方,但是不是任何地方都能通过这个指针变量访问到。数组就是数组,其大小与元素的类型和个数有关。定义数组时必须指定其元素的类型和个数。数组可以存任何类型的数据,但不能存函数。
为什么很多人容易把数组和指针混淆,甚至认为指针和数组是一样的?最主要的原因是它们都可以以指针形式和以数组下标形式这两种形式去访问。
两种访问指针和数组的形式访问一个指针1char *p = "abcdef";
这里定义了一个指针变量 p,本身在栈上占 4 个字节,p 里存储的是一块内存的首地址。这块内存在常量区,其大小为 7 字节。由于这块内存没有名字,对这块内存的访问完全是匿名的。我们可以通过两种方式进行访问:
以指针形式访问
*(p + 4),先取出 p 里存储的地址值,然后加上 4 个字符大小的偏移量,得到新的地 ...
CPU 的两种工作模式:实模式和保护模式
首先我们要知道这两种模式都是 CPU 的工作模式,实模式是早期 CPU 运行的工作模式,而保护模式则是现代 CPU 运行的工作模式。
实模式(Real Mode)起源实模式出现于早期 8086 CPU 时期,8086 也是第一款支持内存分段模型的处理器。当时,8086 只有一种工作模式,即实模式,但当时还没有这个说法。由于 CPU 的性能有限,一共只有 20 位地址线(地址空间只有 1M),以及 8 个 16 位的通用寄存器,以及 4 个 16 位的段寄存器。16 位的物理地址只能访问 64KB 的内存。所以,为了能够通过这些 16 位的寄存器去构成 20 位的主存地址,访问 1 MB 的内存,必须采取一种特殊的方式。
原理第一个字段是由段寄存器提供的,是一个 16 位的段基址。第二字段是段内偏移量,它的值是由通用寄存器(如 EIP)来提供,所以也是 16 位。那么问题来了,两个 16 位的值如何组合成一个 20 位的地址呢?这里采用的方式是:把段寄存器所提供的段基址先向左移 4 位(或乘以 16),这样就变成了一个 20 位的值,然后再与 16 位的段偏移量相加。如下所示:
$$ ...
关于操作系统内存管理的总结
历史许多年以前,当人们还在使用 DOS 或是更古老的操作系统的时候,计算机的内存还非常小。随着应用程序的规模逐渐膨胀,一个难题出现在程序员的面前,那就是应用程序太大以至于内存容纳不下该程序。
通常解决的办法是把程序分割成许多称为覆盖块(overlay)的片段。覆盖块 0 首先运行,结束时他将调用另一个覆盖块。虽然覆盖块的交换是由操作系统完成的,但是必须先由程序员把程序先进行分割,这是一个费时费力的工作,而且相当枯燥。
人们必须找到更好的办法从根本上解决这个问题。不久人们找到了一个办法,这就是虚拟存储器(virtual memory)。虚拟存储器的基本思想是程序、数据、堆栈的总的大小可以超过物理存储器的大小,操作系统把当前使用的部分保留在内存中,而把其他未被使用的部分保存在磁盘上。
比如,对一个 16 MB 的程序和一个内存只有 4 MB 的机器,操作系统通过选择,可以决定各个时刻将哪 4 MB 的内容保留在内存中,并在需要时在内存和磁盘间交换程序片段。而这个 16 MB 的程序在运行前不必由程序员进行分割。
$1K = 2^{10}\ (10\ bits) = 1,024$
$1 ...
学习 i++ 和 ++i 的本质区别
最近在朋友圈看到一篇解释i++和++i的文章,觉得非常有意思,就把要点记录下来。
你的解释不是我想要的…
先看两段代码HelloWorld_1和HelloWorld_2:
123456789// HelloWorld_1.javapublic class HelloWorld_1 { public static void main(String[] args) { int i = 0; i = i++; System.out.println(i); // 0 }}
12345678// HelloWorld_2.javapublic class HelloWorld_2 { public static void main(String[] args) { int i = 0; i = ++i; System.out.println(i); // 1 }}
大家应该一眼就知道运行结果,第一段代码 ...
不要再混淆 C 语言变量的声明和定义
一般情况下,我们对变量的声明(Declaration)和定义(Definition)的理解如下:
声明:用于向程序表明变量的类型和名字。在程序中,变量可以被多次声明。
定义:用于为变量分配存储空间,还可以为变量指定初始值。在程序中,变量有且只有一个定义。
两者主要区别:是否在内存中分配了空间。
只要记住下面的内容即可分清定义和声明:声明相当于普通的声明:它所说明的并非本身,而是描述在其他地方创建的对象。定义相当于特殊的声明:它为对象分配内存。——《Expert C Programming》
进一步解读
定义也是声明,定义变量时我们声明了它的类型和名字。 1int i; // 可以叫「声明」或「定义」,但「定义」更为准确
声明并不一定是定义,如 extern 声明。 1extern int i; // 只能叫「声明」
带有初始化式的声明是定义。 1extern int i = 5; // 只能在函数体外初始化
函数的声明和定义区别比较简单,带有花括号{}的是定义,否则是声明。 12int foo1(int x); // 声明int fo ...
C 语言无符号与有符号整型溢出的区别
无符号整型溢出对于无符号整型溢出,在 C 的规范中这是有定义的行为,溢出后的数会以2^(8 * sizeof(type))作模运算,也就是说,如果一个无符号字符型(1 字节,8 位)溢出了,会把溢出的值与 256 求模。
12unsigned char x = 0xff; // 1111 1111printf("%d\n", ++x); // 结果为:0
有符号整型溢出对于有符号整型的溢出,C 的规范定义是 Undefined Behavior,也就是说,编译器爱怎么实现就怎么实现。对于大多数编译器来说,算得啥就是啥。
注意:用补码表示。
123signed char x = 0x7f; // 0111 1111 表示最大的正数// 0xff就是 -1 了,因为最高位是 1 也就是负数了printf("%d\n", ++x); // -128
++x的结果为1000 0000,因为这是补码,所以值为-128。
所以,有符号整型的溢出规律一般呈环形变化。
如果还是觉得不能理解可以看一下补码的表示方法:关于补码的总结
整型溢出可能导 ...
typedef 的几种用法总结
用途一:简单类型定义一种类型的别名,而不只是简单的宏替换。
1typedef unsigned int UNIT;
可以用作同时声明指针类型的多个对象。
123char *pa, pb; // wrongtypedef char *pchar;pchar pa, pb; // both are pointers to char
#define 是 C 指令,用于为各种数据类型定义别名,与 typedef 类似,但是它们有以下几点不同:
typedef 仅限于为类型定义符号名称,#define 不仅可以为类型定义别名,也能为数值定义别名,比如你可以定义 1 为 ONE。
typedef 是由编译器执行解释的,#define 语句是由预编译器进行处理的。
typedef 如果放在所有函数之外,作用域是从定义开始直到文件结尾,反之则到函数结尾;#define 不管放在哪里,作用域都是从定义开始直到整个文件结尾。
用途二:结构体在以前的 C 语言版本中,声明 struct 新变量时,必须要带上 struct,即形式为:struct 结构名 变量名。
12345struct P ...
C 语言变量内存对齐的作用与规则
现代计算机体系中 CPU 按照双字、字、字节访问存储内存,并通过总线进行传输,若未经过一定规则的数据对齐,CPU 的访址操作与总线的传输操作将会异常的复杂,所以现代编译器中都会对内存进行自动的对齐。
主要作用
平台原因(移植原因)
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。比如,有些架构的 CPU 在访问一个没有进行对齐的变量的时候会发生错误,那么在这种架构下编程必须保证字节对齐。
性能原因
经过内存对齐后,CPU 的内存访问速度大大提升。比如,有些平台每次读都是从偶地址开始,如果一个 int 型(假设为 32 位系统)如果存放在偶地址开始的地方,那么一个读周期就可以读出这 32 位,而如果存放在奇地址开始的地方,就需要 2 个读周期,并对两次读出的结果的高低字节进行拼凑才能得到该 32 位数据。显然,这在读取效率上下降很多。
假设 CPU 要读取一个 int 型 4 字节大小的数据到寄存器中,分两种情况讨论(假定内存读取粒度为 4):
从 0 字节开始:CPU 只需读取内存一次即可把这 4 字节 ...
关于 K&R C、ANSI C、C89、C99 和 C11 的历史总结
K&R C1978年,Dennis Ritchie 和 Brian Kernighan 合作推出了《The C Programming Language》的第一版(按照惯例,经典著作一定有简称,该著作简称为 K&R),书末的参考指南(Reference Manual)一节给出了当时 C 语言的完整定义,成为那时 C 语言事实上的标准,人们称之为 K&R C。从这一年以后,C 语言被移植到了各种机型上,并受到了广泛的支持,使 C 语言在当时的软件开发中几乎一统天下。
C89(ANSI C)随着 C 语言在多个领域的推广、应用,一些新的特性不断被各种编译器实现并添加进来。于是,建立一个新的「无歧义、与具体平台无关的 C 语言定义」成为越来越重要的事情。1983 年,ASC X3(ANSI 属下专门负责信息技术标准化的机构,现已改名为 INCITS)成立了一个专门的技术委员会 J11(委员会编号,全称是 X3J11),负责起草关于 C 语言的标准草案。1989 年,草案被 ANSI 正式通过成为美国国家标准,被称为 C89 标准。
C90(ISO C)随后,TC ...
C 语言全局变量初始化和符号重名的问题
全局变量是 C 语言语法和语义中一个很重要的知识点,首先它存在的意义可以从三个不同角度去理解:
对于程序员来说,它是一个记录内容的变量(variable)。
对于编译/链接器来说,它是一个需要解析的符号(symbol)。
对于计算机来说,它可能是具有地址的一块内存(memory)。
跨单元访问和持续生存周期这两个特点使得全局变量往往成为一段受攻击代码的突破口,了解这一点十分重要。
强弱符号学说在 C 语言里,全局变量如果不初始化的话,默认为 0。int x = 0跟int x的效果看起来是一样的。但其实这里面的差别很大,强烈建议大家所有的全局变量都要初始化,因为编译器在编译的时候针对这两种情况会产生两种符号放在目标文件(*.o)的符号表中,对于初始化的,叫强符号;未初始化的,叫弱符号。
链接器在链接目标文件的时候,如果遇到两个重名符号,会有以下处理规则:
如果有多个重名的强符号,则报错(error);
如果有一个强符号,多个弱符号,则以强符号为准;
如果没有强符号,但有多个重名的弱符号,则「任选一个弱符号」or「先决议到 size 最大的那个,如果同样大小,则按照链接顺序选择 ...
关于 C 语言编译流程 PCAL 的总结
GCC 编译器参考:GCC 编译器介绍(转)
GCC 是 GNU 项目的编译器组件之一,也是 GNU 最具有代表性的作品。在 GCC 设计之初仅仅作为一个 C 语言的编译器,可是经过十多年的发展,GCC 已经不仅仅能支持 C 语言;它现在还支持 Ada、C++、Java、Objective-C、Pascal、COBOL,以及支持函数式编程和逻辑编程的 Mercury,等等。而 GCC 也不再单是 GNU C Compiler 的意思,而是 GNU Compiler Collection 也即是 GNU 编译器家族的意思了,目前已经成为 Linux 下最重要的编译工具之一。
用 GCC 编译程序生成可执行文件看起来似乎只通过编译一步就完成了,但事实上,使用 GCC 编译工具由 C 语言源程序生成可执行文件的过程并不单单是一个编译的过程,而要经过下面的四个过程,可总结为 PCAL:
预处理(Pre-Processing)cpp:.c/.cpp → .i 预处理后
编译(Compiling)cc:→ .s 汇编代码
汇编(Assembling)as:→ .o 机器代码
链接(Link ...
C 语言中变量的四种存储类型
auto
The auto and register specifiers give the declared objects automatic storage class, and may be used only within functions. Such declarations also serve as definitions and cause storage to be reserved.
也就是说动态分配和释放存储空间。当我们定义一个变量时,不给它初始值,它的值是不确定的。
我们之前编写程序的时候很少显式用到 auto 定义变量。如果定义的变量前面没有加static,编译系统会默认为是 auto 的存储方式,会把变量存放在动态存储区。
auto 修饰符的定义里有这么一句「进入包含变量声明的代码时,变量开始存在。当程序离开这个代码块时,变量自动消失。它所占用的内存可被用来做别的事情」。auto 修饰的变量是存储在堆栈中的,而全局变量存储在静态存储区中,所以我们不能用 auto 修饰全局变量。
auto 存储类型说明的变量都是局部于某个程序范围内的,只能在某个程序 ...