C和指针

在C中,我们可以使用指针来访问数据。

完整的说法叫,使用间接访问操作符(也就是*号)进行间接访问。这个指针也叫解引用指针。

所有在内存中的数据无非是0和1,具体表示什么含义需要我们去定义。内存中的值可以表示整数、可以表示浮点数、字符串、甚至是另一个内存的地址。

而例如“int *”的指针用法,表示这个内存存储的值是一个内存地址,而这个内存地址存储的值表示整形。

&表示取地址操作。

*&a=25 和 a=25 的结果是相同的,都是将25赋值给了a。但前者涉及了更多的操作。在不同的编译器(或优化器)的情况下, 如果没舍弃其中多余的操作,产生的机器码可能更慢,更大。而且源码的可读性更差。

对于字面量,需要进行类型转换。*(int *)100=25,这语句表示将25赋值给地址为100的内存。强制转换的过程叫做指向整形的指针。而100是字面量,默认为整形。这种使用方法通常在嵌入式编程中,因为你无法指定内存位置100的内容是什么。而嵌入式编程中,通常可以指定内存存储的东西。

对于两种声明方式,int* a和int *a,有两种看法,第一种是可以看作指向整形的指针,第二种可以看作整形的变量,这个变量代表地址。

C函数的所有调用均是“传值调用”。例如数组名传递的是地址,而地址也是值。函数传递的只是一份指针的值。

C语言的头文件

引入编译器自带的头文件(包括标准头文件)用尖括号。

引入程序自定义的头文件用双引号。

#include <stdio.h>  //引入标准头文件
#include "file.h"  //引入自定义的头文件

使用尖括号,编译器会到指定系统路径下查找头文件。

使用双引号,编译器会根据引入的当前路径查找头文件,如果没有找到,再到系统路径下查找。

系统路径是默认路径,在Windows下这个路径由编译器自己携带。在Linux下这个路径一般固定在某个位置。

其实无论是尖括号还是双引号,都可以用来包含头文件。只要路径正确就行。

#include "D:\\abc\xyz.h"
#include "xyz.h"
#include "include/xyz.h"
#include "../xyz.h"
#include "../include/xyz.h"

可以使用绝对路径来查找头文件。也可以使用相对路径。

通常头文件只写一个文件名,但其实这是./的缩写

#include "./xyz.h"

在window下,文件系统的分隔符可以是正斜杠/,也可以是反斜杠\。无论正斜杠还是反斜杠都可以有一个也可以有两个。

在Linux下,文件系统分隔符只能是一个正斜杠。

C函数的参数传递

C函数的参数分为实参和形参。

总的来说,实参和形参是分开的,形参的值无论如何都不会改变实参值。但可以通过指针改变指针指向的内存地址的值。

C语言采用的是值传递,无论形参如何变化,都不会改变实参的值。如果参数是指针,改变的是指针所指的地址中的内容。但参数的指针无论如何变化,都不会改变。

首先来看普通变量的参数传递。

void func(int arg) {
	arg += 1;
}

void main(){
	int a = 0;
	func(a); // a的值为0
}

变量a初始化为0,执行func函数后依旧为0。因此在普通变量下实参和形参不影响。

那如果数组变量呢?

void func(int arg[]) {
	arg[0] += 1;
	arg[1] += 1;
}

void main(){
	int a[] = { 0, 1};
	func(a); // 数组a的值变成了{1, 2}
}

为什么数组的值改变了呢?因为数组本身是指针变量。数组名表示的是数组的首地址,而数组后面的下标表示数组地址上的值。我们调用函数时传递的是数组的首地址,而函数改变的是这个地址上后续的值。

void func(int arg[]) {
	arg += 1;
}

void main(){
	int a[] = { 0, 1};
	func(a); // 数组a的首地址保持不变
}

上面的代码执行后a的首地址保持不变。其中有趣的地方在于形参。

我们知道,变量的地址是无法通过赋值改变的,但指针的地址可以。下面的代码编译器是无法通过的,因为赋值语句在尝试修改数组的首地址,这是不允许的。

void main(){
	int a[] = { 0 };
	a += 1; // 编译器无法通过
}

但上面的代码中,函数func拥有形参数组a,而在函数体中,修改了数组的首地址,这语句编译器通过了。说明形参“int a[]”的本质不是数组,而是指向数组首地址的指针。

因为指针变量允许修改变量中的地址值。在不用const修饰的情况下,也允许修饰地址指向的内存的值。

对于数组来说,可以把形参都看成临时的指针变量,无论如何修改指针的变量,改变的只是当前指针的值。而不会改变指针指向地址的值。也就是不会改变数组的值。

只有修改指针变量指向内存地址的值,实参才会改变。


既然数组的本质是地址,那形参作为指针变量也很容易理解了。

void func(int* arg) {
	arg += 1;
}

void main(){
	int a = 0;
	func(&a); // a的值依旧是0
}

但如果使用的是星号,那变量a的值会发生改变。

void func(int* arg) {
	*arg += 1;
}

void main(){
	int a = 0;
	func(&a); // a的值变为了1
}

C语言中const关键字

C语言中的const关键字表示这个变量的值在其整个作用域中都保持不变。

但const是在语义层面的检查,是编译器在编译期间,用语法检测来保证数据没有被修改。但在运行时,const修饰的变量和其他未被const修饰的普通变量一样是放在内存中的数据区。必要时可以通过修改内存来达到修改值的目的。

void main(){
	int a = 0;
	a = 1;
}

一个变量有两个维度来描述,一个是地址,另一个是这个地址上的值。上面这段代码,我们在变量a的地址上修改了它的值,由0修改为了1。

void main(){
	const int a = 0;
	a = 1; //编译时不允许通过
}

上面的代码结果是显而易见的,编译无法通过。因为代码虽然没有尝试修改a的地址,但修改了a的值。

既然const关键字限制了变量值的修改,那变量的地址呢?

void main(){
	int a = 0;
	&a = &a; // 编译器不允许通过
}

变量的地址本身是无法修改的,因为同样不被编译器允许,即使这个地址就是它自身的地址。


既然const修饰的普通变量值和地址都无法修改,那数组变量值修改呢?我们先来看没有const修饰的数组。

void main(){
	int a[] = { 0, 0 };
	a[0] = 1;
	a[1] = 1;
}

对于数组也需要分为两个维度,一个是数组的首地址,另一个是数组的值。对数组来说,数组名表示的是数组的首地址,而数组的值和数组的数据类型有关,但最终也表示数值。

void main(){
	const int a[] = { 0, 0 };
	a[0] = 1; // 编译时不允许通过
	a[1] = 1; // 编译时不允许通过
}

上面的数组用const修饰后,结果也很明显,编译无法通过。因为我们无法修改数组的值。

而对于数组的地址,自然也是无法编译通过。

void main(){
	int a[] = { 0, 0 };
	int b[] = { 1, 1 };
	a = b; // 编译无法通过
}

因为数组名代表的就是数组的首地址,“a = b”这条语句相当于把数组b的地址赋值给了数组a 的地址。而普通变量代表的是这个地址上的值,普通变量名的赋值操作是值对值,而数组名的赋值操作时地址对地址。

void main(){
	int* a = 0;
	int b[] = { 1, 1 };
	a = b; // a的值为数组b的地址
}

由于编译器的原因,这其中的细节被隐藏掉了,导致看起来不太习惯。这也是我们对数组初始化只有用for循环或内存操作memset()的原因。

说回const的话题。如果数组存放的是指针呢?能否修改其中的值?

void main(){
	const int* a[] = { 0 };
	int b = 0;
	a[0] = &b;
}

上面的代码是可以通过编译的,最终a[0]的值为整型变量b的地址。但如果尝试修改b的值呢?

void main(){
	const int* a[] = { 0 };
	int b = 0;
	a[0] = &b;
	*a[0] = 1; // 编译器不允许通过
}

上面这段代码编译器不允许通过,因为我们尝试修改了指向整型变量的地址上的值。我们回到普通的变量来看针对指针变量的情况。


首先我们要知道,变量的地址本身是无法修改的,因为同样不被编译器允许。

void main(){
	int a = 0;
	int b = 0;
	&a = &b; // 编译器不允许通过
}

既然const修饰的普通变量的值我们无法修改,那指针变量呢?

void main(){
	const int a = 0;
	int b = 0;
	a = &b; // 编译器不允许通过
}

上面这段代码很清晰,const修饰的整型变量a的值不允许修改,因此将b的地址赋值给a的操作是不被允许的,编译器自然无法通过。

但如果a是指向整型变量的指针类型呢?例如下面这段代码。

void main(){
	const int* a = 0;
	int b = 0;
	a = &b;
}

很奇怪的是,编译器通过了。指针变量a表示的是,这个变量存放着指向整型变量的地址。但这个地址的值是允许改变的。

那什么情况下不允许改变呢?

void main(){
	const int* a = 0;
	int b = 0;
	a = &b;
	*a = 1; // 编译器不允许通过
}

当我们尝试去修改指针变量a所指向的地址的值时,这个情况是不允许的。上面的代码相当于尝试修改变量b的值,但被编译器拒绝了。

当没有const修饰时,b的值可以被正确修改。

void main(){
	int* a = 0;
	int b = 0;
	a = &b;
	*a = 1; // 变量b的值被修改为了1
}

由此看来。const并不是完全限制当前变量的值的修改。

而是,当const修饰的变量是普通变量时,不允许修改其中的值。当const修饰的是指针变量时,不允许修改这个指针指向的地址上的值。

而针对多级指针,也是如此。

void main(){
	const int** a = 0; // 二级指针
	int* b = 0;
	int c = 0;
	a = &b;
	b = &c;
	** a = 1; // 编译不允许通过
}

上面二级指针变量a指向了b,b指向了c。如果没有const修饰,最终变量c的值为1。但当有const修饰时,编译器不允许代码修改最终的变量的值。

C语言中的二、八、十、十六进制。

##C语言中的二、八、十、十六进制。

我们都知道无论是什么样的值,在计算机中都是以0和1储存的。基于一串0和1组成的数,我们根据我们的定义或规定来表示,才能变成我们想要的“数据”。

下面是我们的输入,我们可以根据我们的需要输入不同进制的数。

#include <stdio.h>
int main()
{
	int a = 0b00010100; // 二进制
	int b = 0000000010;// 八进制
	int c = 1000000000;// 十进制
	int d = 0x00010100; // 十六进制
	printf("%d\n", a); // a为十进制的20
	printf("%d\n", b); // b为十进制的8
	printf("%d\n", c); // c为十进制的1000000000
	printf("%d\n", d); // d为十进制的65792
}

0b开头且0和1组成的数为二进制;0开头且0~7组成的数为八进制;1~9开头且1~9组成的数为十进制;0x开头且0~F组成的数为十六进制。

逻辑运算和位运算——与或非、左移和右移

##逻辑运算和位运算——与或非、左移和右移

与:当且仅当两个数的位都为1时,为真。

或:两个数只要任意一个为1时,就为真。

非:真为假,假为真。


这次重点还是放在左移运算和右移运算上。下面代码的注释简化了int型的字节数。int实际上是4个字节。

左移运算:

#include <stdio.h>
int main()
{
	int a = 0b00010100 << 2; // 左移两位
	printf("%d\n", a); // 二进制0b01010000
}

数据左移时候,最左边位将被丢弃,同时在最右边补0。

右移运算:

#include <stdio.h>
int main()
{
	int a = 0b00010100 >> 2; // 右移两位
	printf("%d\n", a); // 二进制0b00000101

	int b = -10 >> 2; // 十进制为-10,二进制为0b11110110
	printf("%d\n", b); // 十进制为-3,二进制为0b11111101
}

右移的时候,最右边的位将被丢弃,但右移要稍微复杂一些。如果数字是一个无符号数值,则用0填补最左边位。如果数字是一个有符号数值,则用根据符号位填补最左边的位。也就是说,对于无符号数,用0填补最左边的位,丢弃最右边的位;对于有符号数,用1填补最左边的位,丢弃最右边的位。

逻辑运算和位运算——异或

##逻辑运算和位运算——异或

如果a、b两个值不相同,则异或结果为1。如果a、b两个值相同,异或结果为0。

更通俗的讲就是:两个值不同为真,两个值相同为假。

10
10(假)1(真)
01(真)0(假)
异或运算真值表

上面是异或在逻辑运算中的使用。但如果异或放在位运算中,就有很多的应用场景。


我们把上面的表抽取出来,看成位运算的话可以得到下面两个表,并得出两个简单结论:

10
010
用0进行按位异或运算

结论一:如果一个数用0进行按位异或的话,会得到这个数本身。

10
101
用1进行按位异或运算

结论二:如果一个数用1进行按位异或的话,会得到相反的数。也就是取反。


用上面的两个结论,我们有四种基本的异或使用环境:

使用环境一:使某些特定的位取反。

a = 10100001;
b = 00000110;
c = a ^ b; // 0b10100111

使用环境二:实现两个值的交换,而不必使用临时变量。

a = a ^ b; // a=0b10100111
b = b ^ a; // b=0b10100001
a = a ^ b; // a=0b00000110

使用环境三:将变量自身置零。

a = 10100001;
a = a ^ a; // 0b00000000

使用环境四:快速判断两个值是否相等。

if ((a ^ b) == 0)
{
	//如果a和b相同则继续执行
	...
}

从上面的几个基本使用方法还可以衍生出一些高级的用法。

用法一:如异或的加密运算。

#include <stdio.h>

int main()
{
	char a = 'w'; // 原文
	char secret = '8'; // 密钥

	a = (char)(a ^ secret); // 加密
	printf("%c\n", a); // 大写字母O
	a = (char)(a ^ secret); // 解密
	printf("%c\n", a); // w
}

这里使用了异或取反的特点。用同一个密钥(在上面代码中为变量secret),对相同的位进行异或取反两次就可以得到原文。也就是负负得正的原理,“负”一次为密文,再“负”一次就是原文了。

用法二:检测两个数不同的比特位位置。

int a = 0b10100001;
int b = 0b10101011;
int c = a ^ b; // 0b00001010

根据结果我们可以看到数a和数b,第1位和第4位的比特位是不同的。当且仅当只有一个数的某位上为1时,结果的该位才为1。否则结果的该位为0。

用法三:数a需要改变多少个比特位才能得到数b。

这是上面用法的扩展。

#include <stdio.h>

int main()
{
	int a = 0b10100001;
	int b = 0b10101011;
	int c = a ^ b; // 0b00001010

	int count = 0;
	while (c)
	{
		c &= (c - 1);
		count++;
	}
	printf("%d", count); // count的值为2
}

在上面代码中,数a和数b异或的结果c为0b00001010 。结果c表示,数a需要根据数c,在数c比特位为1的对应位置取反,就可以得到数b。具体到上面的代码中,数a需要在第1位和第4位的位置取反,就可以得到数b。而代码中while循环和其中count的意思是,数a需要改变2个比特位才能得到数b。

C语言中联合union的正确用法。

##C语言中联合union的正确用法。

假设你想用结构来模拟某样东西。但这个东西的属性可以用多种的数据类型来表示。

例如,一箱苹果你可以几种数据类型来表示:

  • 1、一箱有10个苹果。(int)
  • 2、一箱有2.5斤苹果。(float)
  • 3、一箱有1.25公斤苹果。(float)

如果使用常规的结构体来表示的话:

typedef struct {
	int number; // 苹果个数 4字节
	float weight_half_KG; // 苹果重量(斤) 4字节
	float weight_KG; // 苹果重量(公斤) 4字节
}Apple;

这不是一个好主意,因为结构体在内存中占用了更多的空间(结构体Apple占用了12个字节),无论用户是只使用了一个字段,还是三个字段都使用了。用户可能会既设置一箱苹果的数量,也设置了重量。

因此,我们会自然而然的想,如果有一种数据类型,可以根据我给出的数据来决定是使用何种数据类型就好了。当我给出的是整形数据,那就使用“int number”;如果我给出的是浮点数数据,根据需要使用“float weight_half_KG”或“float weight_KG”。而C语言中的联合就可以做到这点。

当定义联合时,计算机会为根据其中最大的字段分配空间。

typedef union {
	int number; // 苹果个数 4字节
	float weight_half_KG; // 苹果重量(斤) 4字节
	float weight_KG; // 苹果重量(公斤) 4字节
}Apple;

在这里三个字段的都是4字节,因此计算机会分配4字节的内存空间给我们定义的联合变量使用。无论是设置了number,还是weight_half_KG,还是weight_KG,他们都会保存在内存中的同一个地方。而且长度只有4个字节。

我们可以使用整形的变量:

int main()
{
	Apple a;
	a.number = 10;  // 一箱苹果有10个
}

我们也可以使用浮点数的变量:

int main()
{
	Apple a;
	a.weight_half_KG= 2.5; // 一箱苹果有2.5斤
}

获取联合成员的方式和获取结构成员的方式一。唯一的差异在于,当改变一个联合成员的值时,实际上修改了该联合所有成员的值。

int main()
{
	Apple a;
	a.weight_KG= 1.25; // 一箱苹果有1.25公斤

	printf("%p\n", &a.number);
	printf("%p\n", &a.weight_half_KG);
	printf("%p\n", &a.weight_KG);
}

联合中的所有字段都可以访问,但它们会指向同一个地址。只是它们在使用时表现的数据类型不一样。

计算机需要保证联合的大小固定。唯一的办法就是使用最大的内存,让任何一个字段都装得下。如果定义的时候没有指定初始化,会被隐式地初始化为0值

联合提供了一种方法,一种可以用不同数据类型创建变量的方法。而且联合经常会和结构体一起使用。

C语言中static关键字的正确用法。

##C语言中static关键字的正确用法。

如果你想要创建一个带有计数功能的函数,你可以这么写:

int count = 0;
int counter()
{
	return ++count;
}

这段代码功能上没什么问题。但它使用了一个叫count的全局变量。count的作用域是全局。这意味着所有函数都可以修改count的值。在一个大型程序中,需要非常小心的控制全局变量的个数,因为它们可能导致代码出错。

但C语言允许你创建只能在函数局部作用域访问的全局变量。

int counter()
{
	static int count = 0;
	return ++count;
}

使用static关键字可以把变量保存在全局量区中(可以当成全局变量),但是当其他函数试图访问count变量时编译器会抛出错误。

如果你调用上面代码中的函数,count的值不会被反复定义(初始化)。

#include <stdio.h>

int main()
{
	int a = 0;
	a = counter(); // 此时a的值为1
	a = counter(); // 此时a的值为2
	printf("%d",a);
}

##用static定义私有变量或函数。

也可以在函数外使用static关键字,它表示“只有这个.c文件中的代码可以使用这个变量(或函数)”。举个例子:

static int days = 365;
static void update_account(int x)
{
	...
}
// 变量days和函数update_account只有当前.c文件可以访问。

static关键字可以用来控制变量或函数的作用域。可以防止其他代码以意想不到的方式访问你的数据或函数。

C语言中的一些小问题。

##C语言中的一些小问题

#头文件的名字用双引号括起来,而不是尖括号,它们的区别是什么?

#include <stdio.h>
#include "totaller.h"

当编译器看到尖括号,就会到标准库代码所在目录查找头文件。如果用引号把文件名括起来,编译器就会在本地查找文件。

但严格来说,这是由编译器的工作方式决定的。通常情况下,引号表示以相对路径查找头文件,如果不加目录名,只包含一个文件名,编译器就会在当前目录下查找头文件;如果用了尖括号,编译器就会以绝对路径查找头文件。

gcc知道标准库的绝对路径。如果你在Windows中安装了MinGW编译器,那标准库头文件stdio.h通常就在安装目录下C:\MinGW\include\stdio.h中。

#什么是预处理?

预处理是在正式编译开始之前修改代码,创建新的源文件。例如预处理会读取头文件中的内容,插入主文件。但预处理并不会真的创建一个文件。为了提高编译的效率,编译器通常会用管道在两个阶段之间发送数据。

#为什么C语言中是使用复制的方式把值给形参?

因为计算机是通过赋值的方式向函数传递值的。所有赋值都会复制值。