据非官方不负责任的统计,有百分之八十的 Android 应用开发者,不会或者很少有机会写 C/C++。
如果你是一个有追求的程序员,还是把 C 语言捡起来吧。

源码

本文所有涉及的所有源码:https://github.com/gavinliu/Study-C-CPP

数据类型

C 语言是一个有类型的语言,使用变量必须定义确定类型。

  • 整数:char, short, int, long, long long, bool
  • 浮点数:float, double, long double
  • 指针
  • 自定义类型

数据类型的不同

在 64 位机器上:

数据类型 内存长度 格式化
char 1 Byte %d
short 2 Byte %d
int 4 Byte %d
long 8 Byte %ld
float 4 Byte %f
double 8 Byte %lf

整数和浮点数 在内存中的形式都是以二进制的形式存在的,但是整数是真实的二进制数字 可以直接拿来运算,而浮点数是一种编码方式,不能直接运算。通常来说 CPU 上专门有一个硬件来处理浮点数运算。

sizeof

这个方法可以返回某个类型或者某个变量在内存中所占据的字节数

1
2
3
sizeof(int);
int a = 0;
sizeof(a);

整数

int 的特殊意义

int 和 long 的长度取决于编译器和 CPU,通常的意义是表示 “一个字” 的长度。

什么叫一个字?

CPU 和内存读取数据模型:

CPU [register] <—-总线—-> RAM

CPU 中的寄存器通过总线和内存进行数据读写,现在的 CPU 的寄存器通常这个大小是 32bit 或者 64bit,这个大小就是一个字的长度,通常就代表 int 的长度。

内部的表达

在计算机中任何数据都是二进制数据,这个数据是什么取决与我们怎么看它。

1 个字节可以表达的数范围:

00000000 ~ 11111111 (0~255)

因为 char 刚好就是一个字节,我们就拿 char 来实验一下:

因为一个字节,只能表达 256 个数字,那么 char 的表示数的范围是 (0 ~ 255) 还是可以表示负数呢?

1
2
char c = -1;
printf(c="%d\n", c);
1
c=-1

测试代码可以看出 char 是可以表示负数的。

那么负数怎么表示呢?对应的二进制又是怎样的呢?

1 –> 00000001
0 –> 00000000
-1 –> ?

我们这样这样想 -1 + 1 = 0,那么:

1
2
3
  ????????
+ 00000001
= 00000000

这样看就很明显了:

1
2
3
4
  11111111
+ 00000001
=100000000
= 00000000

100000000 由于 超出了 1 个字节的范围,所以溢出了就变成了 00000000

11111111 和 00000001 是什么关系呢?

原码:00000001
反码:11111110
补码:11111111

所以虽然内存中的数据都是 11111111,但是当被当成原码看的时候是 255,当成补码看的时候就是 -1

signed & unsigned

signed, unsigned 数据类型的修饰符,只能修饰 整数类型的数据,表示是否带符号,也就是不用补码来表示负数了。

8 进制 16 进制

1
2
3
4
int i = 012; // 八进制
int j = 0x12; // 十六机制

printf("i=0%o,j=%x\n", i, j);

浮点数

精度误差

1
2
3
4
5
6
7
8
9
float f = 1.123f;
float ff = 2.123f;
float sum = f + ff;

if (sum == 3.246f) {
printf("相等\n");
} else {
printf("不相等 sum=%.10f, sum=%f\n", sum, sum);
}
1
不相等 sum=3.2459998131, sum=3.246000

浮点数是不准确,有效位数是有限的,所以在处理钱的问题上,不要使用小数点来表示角分,而是转换成最小单位来计算,比如:
1.23 元 = 123 分,这样转换成整数计算就不会又误差了。

指针

指针是什么? 指针就是一个内存地址,代表的就是一块内存空间。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 定义一个 int类型的变量 i 值为5
int i = 5;
// &i 中的 &为取地址符,可以得到 i 的地址,%x 为十六进制输出
printf("i的地址 %p\n",&i);

// 指针变量 定义一个int* 类型的变量p
int* p;
// 把 i 的地址给 p,现在 p 就存放的 i 的地址
p = &i;

printf("i=%d\n",i);
// *p --> 当指针变量前面有*的时候,表示取这个指针变量所存放的地址里面对应的数据
printf("*p的值%d\n",*p);

指针占多少字节

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int i =3;
double d = 3.141692;
float f = 3.1423;
char c ='B';

int* ip = &i;
double* dp = &d;
float* fp = &f;
char* cp = &c;

printf("int 类型指针变量的长度为 %d\n",sizeof(ip)); // --> 4
printf("double 类型指针变量的长度为 %d\n",sizeof(dp)); // --> 4
printf("float 类型指针变量的长度为 %d\n",sizeof(fp)); // --> 4
printf("char 类型指针变量的长度为 %d\n",sizeof(cp)); // --> 4

数组

数组变量是特殊的指针

  • 无需用 & 取地址
1
int a[10]; int *p = a;
  • 但是数组的单元表达的是变量,所以是需要用 & 取地址
1
*a == &a[0]
  • [] 可以对数组做,也可以对指针做。
1
p[0]; a[0];
  • * 也可以对数组做
1
*a = 0;
  • 数组变量是一个 const 的指针,所以不能被再次赋值,只能初始化。
1
2
int b[] = a;
// int b[] === int * const b

指针运算

1
2
3
4
5
6
7
8
9
10
// 指针的运算和数组都是紧密关联的
char* pChar = &arr[2];

printf("pChar的内存地址:%#X,对于的值为:%c\n", pChar, *(pChar));
printf("pChar + 1 的内存地址:%#X,对于的值为:%c\n", pChar + 1, *(pChar + 1));

// pChar的内存地址:0X28FF22,对于的值为:c
// pChar + 1 的内存地址:0X28FF23,对于的值为:d

// 指针的运算 按照 约定好的数据类型, 偏移相对应的内存空间的大小 !
1
2
printf("dist=%c\n", &arr[0] - &arr[2]); // dist=2
// 指针减法 返回的值是距离,并不是返回整数相减的结果

指针类型转换

1
2
int* p = &i;
void* q = (void*) p;

函数指针

1
2
3
4
5
6
7
8
9
10
void print(char const * str) {
printf("%s\n", str);
}

void sayHi(void (* p)(char const * str)) {
p("hi2");
}

sayHi(print);

指针的运用

  1. 需要传入较大的数据时用作参数:传数组
  2. 对数组的操作
  3. 函数返回值不止一个结果的时候
  4. 需要用函数来修改不止一个变量:swap
  5. 动态申请内存
  6. 函数指针相当于 java 的回调

动态内存分配

1
2
3
4
5
6
7
#include <stdlib.h>

// 申请 n个int 大小内存,失败返回 0
int* p = (int*) malloc(n * sizeof(int));

// 回收
free(p);

字符串

声明字符串的三种方式

1
2
3
4
5
6
7
8
char c[] = {'H', 'e', 'l', 'l', 'o', '\0'};
printf("%s\n", c);

char c2[6] = {'H', 'e', 'l', 'l', 'o'};
printf("%s\n", c2);

char* str = "Hello World!";
printf("%s\n", str);

字符串数组

1
2
char s[][] = {"Hello", "Wrold"};
char *ss[] = {"Hello", "Wrold"};

字符串操作

1
#include <string.h>

在标准函数库 string.h 里面有关于 string 处理相关的函数库。

具体方法查找:http://www.runoob.com/cprogramming/c-standard-library-string-h.html

结构类型

枚举

1
2
3
4
5
6
enum Type {
Open,
Close
};

enum Type t = Open;

结构体

1
2
3
4
struct Position {
int x;
int y;
};

结构体的内存大小为一般来说所有成员的大小之和,但是也会存在内存对齐的情况:

  1. 结构体变量中成员的偏移量必须是成员大小的整数倍(0 被认为是任何数的整数倍)
  2. 结构体大小必须是所有成员大小的整数倍,也即所有成员大小的公倍数。

结构体初始化

1
2
3
4
5
struct Position p = {1, 2};

struct Position q;
q.x = 2;
q.y = 2;

结构体指针

1
2
3
4
struct Position* p = &q;

(*p).x = 1;
p->y = 2;

C 的 -> 操作符,其实就是指针取值的简写。

结构体运算

1
2
p = (struct Position) {1, 2}; // 相当于 p.x = 1; p.y = 2;
p1 = p2 // 相当于 p1.x = p2.x; p1.y = p2.y

联合体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
union Value {
int x;
float y;
char z;
double q;
};

union Value v;

v.x = 1;
printf("%d, %f, %c, %f\n", v.x, v.y, v.z, v.q);

v.z = 'A';
printf("%d, %f, %c, %f\n", v.x, v.y, v.z, v.q);

v.y = 0.1;
printf("%d, %f, %c, %f\n", v.x, v.y, v.z, v.q);

v.q = 0.2;
printf("%d, %f, %c, %f\n", v.x, v.y, v.z, v.q);

printf("%zu\n", sizeof(union Value));

联合体的大小,会存在内存对齐的情况,其内存大小等于成员中最大的那个属性的长度,并且因为联合体内的成员是共用内存的,所以只会安全的保存一个值。

1
2
3
4
5
1, 0.000000, , 0.000000
65, 0.000000, A, 0.000000
-1717986918, 0.100000, �, 0.100000
-1717986918, 0.200000, �, 0.200000
8

别名

1
2
3
4
5
6
7
8
9
typedef int Age;

typedef int* AgeP;

struct Man {
char* name;
};

typedef struct Man Student, *StudentP;
1
2
3
4
5
6
7
8
9
10
11
12
Age a = 1;
printf("%d\n", a);

AgeP p = &a;
printf("%d\n", *p);

Student student;
student.name = "Jack";
printf("%s\n", student.name);

StudentP sp = &student;
printf("%s\n", sp->name);
1
2
3
4
1
1
Jack
Jack

给结构体取别名,可以让结构体的使用更加接近于 Java。

宏定义

1
#define value

下面这个是 标准 C header 的宏定义,为了解决头文件的导入死循环

1
2
3
4
5
6
#ifndef __X__HEADER__
#define __X__HEADER__

void hello();

#endif

在 C99 之前,C 语言没有 const 关键字,所以定义常量通常也是用宏定义

1
#define PI 3.14159

编译器处理宏定义其实就是最原始的文本替换。

x.c
1
double x = 2 * PI;

==>

x.i
1
double x = 2 * 3.14159;

带参数的宏

1
2
3
#define cube(x) ((x)*(x)*(x))

printf("%d\n", cube(2)); // 8