StarrySky's Blog


  • 首页

  • 归档

  • 分类

  • 标签

  • 关于

  • 搜索

C++语言学习笔记一C语言部分

发表于 2017-06-24

学习材料是”C语言深度剖析”

补码

按我自己的理解, 补码其实很简单, 就是一个用无符号数(状态)表示有符号数的过程,因为计算机底层0101本身就没符号的概念,所以将有符号数建立在无符号数基础上也很自然:
128 |
129 | 127
130 | 126
131 | 125
… | …
254 | 2
255 | 1
| 0
注: ‘
‘代表没有对应值, 同一行的两个数相加恒等于256(0除外);
如上图所示,我们以一个字节为例,也就是char类型, 竖线右边的值对应二进制最高位为0, 左边对应二进制最高位为1, 更宽的数据以此类推:

  • 当为unsigned char时: 数据范围从0逆时针到255,一共256种状态.
  • 当为signed char/char时,数据范围从(-128) - 127, 竖线左边为负值对应的补码(其原码就是对应的竖线右边的那个值取负, 如254对应的原码就是-2), 右边为正值补码(正值的补码=原码)
  • 对于8bits, 0-255就是所有的256中状态,计算机只存储0-255对应的二进制, 如果要用有符号, 我把左边解析成负数,右边解析成正数就行了.
  • 有了上面的图解析过程就很简单了:
    • 从内存取出二进制值(补码) val;
    • 当V的最高位为0时, 就把V的值作为解析结果;
    • 当V的最高位为1时, 就把V-256的值作为解析结果;
  • 问题是第三步, 总不能为了解析一个数,再让CPU去做以此减法吧, 正好(~val)+1 <=> 256-val => val-256 <=> -((~val)+1) (~val是val的二进制按位取反), 按位取反对计算机来说那就so easy了.

C语言32个关键字:

数据存储类别 : auto static register extern
基本数据类型 : void char int float double
类型修饰符 : short long signed unsigned
类型限定符 : const volatile
组合数据类型 : struct union enum
控制语句 : if else do while for switch case default break continue goto return
其它 : typedef sizeof

数据存储类别指示符:

C语言声明变量有存储类型指示符:extern, static, auto, register,不指定时编译器根据约定自动取缺省值auto。
存储类型指示符的位置是任意的,仅对于存储类型指示符而言:

1
static const int i <=> const static int i

C语言规范中,进行语法分析的时候,typedef和数据存储类别指示符是等价的, 仅对于typedef而言:

1
2
typedef const int i <=> const typedef int i <=>
int const typedef i <=> const int typedef i

typedef不能和static等存储类型指示符同时使用,因为每种变量只能有一种存储类型.

1. static(静态变量):

  • 修饰局部变量,扩展其生存期到整个程序生存期,但变量作用域不变,作用域外无法访问,位于静态存储区;
  • 修饰全局变量或函数,限制其作用域,由整个程序或者项目到仅限于所在cpp文件(对应于内部链接);
  • [C++]修饰类的的数据成员和成员函数,表示数据唯一性,即该数据成员或者成员函数属于类而不是某个对象.

    注: linkage(链接性): 描述某个标识符(或名称)在整个程序或者某个源文件中能否绑定到同一实体(某块内存,如具体的变量以及函数体等)
    无链接性:
    局部变量(包括类的非静态成员), 函数形参.
    内部链接(internal linkage): 标识符只能绑定到某个编译单元(预处理后的某个源码文件)内部的实体,不能跨编译单元:
    a. 静态全局变量, 全局const常量(C++,C中还是外部链接);
    b. inline函数(自由函数和类成员函数), 静态自由函数;
    外部链接(external linkage):标识符可以跨编译单元绑定到某一实体:
    a. 非静态全局变量, 类的静态数据成员;
    b. 非静态自由函数,类成员函数和类静态成员函数;

2. extern(声明其具有外部链接属性,同static相反):

  • 声明本编译单元中的全局变量(一般用于全局变量定义之前使用该全局变量)
  • 声明其它源文件中的全局变量(注意这里的源文件是指cpp文件,不是头文件,头文件直接include就行了,不需要用extern);
  • extern与const同时使用,表示具有外部链接属性的常量.
  • extern “C” {}:用于C++代码,告诉编译器{}中的代码编译时不做名称改写,防止由于C++的名称改写而导致的连接错误;一般用于C语言写的库(要在头文件的声明中使用),并且允许它用于C++语言时.

类型限定符:

1. const(只读变量):

  • 实质还是变量,只是不能修改(C中可通过指针间接修改);
  • C中不能用const变量来指定数组元素个数,但C++中可以;
  • 分析const的作用对象时,忽略基本类型说明符即可,所以

    1
    const int i; <=> int const i;
  • 可以用来说明只读数组;

  • 修饰指针:

    1
    2
    3
    4
    const int * p; // => const (*p) <= 指针指向的内存不可修改
    int const * p; // => const (*p) <= 指针指向的内存不可修改
    int * const p; // => * (const p) <= 指针变量本身的值不可修改
    const int * const p; // => const (* (const p)) <= 指针变量本身和指针指向的内存都不可修改
  • 修饰函数参数: 防止实参被函数修改

  • 修饰函数返回值: 返回值不可被改变,一般用于返回引用的函数,防止外部修改被引用的变量.
  • const与#define
    编译器不会为const只读变量分配存储空间,而是放在符号表中,效率高, const定义的只读变量从汇编角度来看,只是给出了对应的内存地址, 而不是像define一样给出的是立即数,const只读变量在程序运行期间只有一份拷贝, 因为它是全局只读变量, 存放在静态区:
    1
    2
    3
    4
    5
    6
    7
    #define M 3 // 宏常量
    const int N = 5; // 此时未将N放入内存中
    // ... main ...
    int I = M; // 预编译期间进行宏替换,分配空间
    int J = M; // 再进行宏替换,又一次分配内存
    int i = N; // 此时为N分配内存,以后不再分配!
    int j = N; // 没有内存分配

2. volatile(关闭优化,让编译器老老实实按语句的意思来编译):

1
2
3
4
5
[global] int i = 0; | |
[local] i = 20; | |
| <====== | i = 5; // 端口状态发生改变
int j = i; | |
int k = i; | |
= 20;``` 从内存中取出i的值赋值给i;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
```int j = i;``` 使用前面取出的值而不是重新从内存读取值,如果此时端口值(i)发生变化,那此处使用的i就不是i的真实值; 如果把```int i = 0``` 改为 ```volatile int i = 0```,那编译器就不会对i做优化,此时会从内存中重新读取i的值,然后给j赋值.
### 组合数据类型:
#### 1. 结构体(struct):
* 空结构体大小为1;
* **柔性数组**:当结构体的**最后一个元素(并且不是第一个,前面有其它成员)**是数组时,允许它不指定大小,即它的大小是可变的,成为柔性数组
- sizeof带柔性数组的结构体,大小不包括柔性数组大小;
- 带柔性数组的结构体需要用malloc进行内存分配,并且分配的大小要大于结构体大小,多出来的部分就是柔性数组的大小;
#### 2. 结构体的内存对齐:
默认情况下,为了方便对对结构体内元素的访问与管理,当结构体内的元素长度都小于处理器的位数时,便以结构体里最长的元素为其单位,即结构体的长度一定是最长数据元素的整数倍;
如果结构体里最长元素的内存长度大于处理器位数,就以处理器的位数为对齐单位。
并不是处理器位数,是看**操作系统的位数**
```C
// manjaro x86_64 -> 64 bits = 8 bytes
struct foo1 { | base_addr: 0
char *p; // 8 bytes | offset: 0
char c; // 1 bytes | offset: 8
// char pad[7] // padding 7 bytes |
long x; // 8 bytes | offset: 16
};
// total 24 bytes
struct foo3 {
int i; // 4 bytes (min(4,8) = 4)
char c; // 1 bytes
/* char pad[3] */ // 结构体的尾填充 3 bytes
};
// total: 8 bytes

foo5和foo6中的最长元素长度是int的长度(换句话说看的是类型长度,不是变量长度),所以foo5和foo6的长度一定是4的整数倍。
对于位域,比如foo6中a和b的类型是int,占4个字节,而a、b只占去了1个字节,占去的是这四个字节中的低位字节,也就是说,位域是通过偏移来实现的,而这个偏移是从低位向高位偏移,即a占据低位的0-4bits,b占据低位的5-7bits;
对于位域,我觉得只要把它以bytes为基本单位来分析,同时时刻铭记它是在一个内存模型(数据类型)中即可.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
struct foo5 {
short s; // 2 bytes
char c; // 1 bytes
int flip:1; // 1 bits
int nybble:4; // 4 bits
int pad1:3; // padding 3 bits // 用来与char c对齐,看成一个独立的bytes
int septet:7; // 7 bits
// int pad2:17; // padding 17 bits // 4bytes是一个集体,最后没用完的要填充,形成一个完整的4bytes
// char pad3; // padding 8 bits
};
// total: 8 bytes
---------------------------------------------------------------
| short | char |1| 4 |3| 7 | 17 | char |
-----------------------|-------|---------------------|--------|
1 bytes 3 bytes 1 bytes
|------------------------------|------------------------------|
4 bytes 4 bytes
struct foo5_2 {
short s; // 2 bytes
char c; // 1 bytes
int flip:2; // 2 bits ------------|
// int pad1:6; // padding 6 bits ----| total 1 byte ---|
int nybble:9; // 9 bits ---| |
// int pad2:7; // ----------| total 2 bytes -----------|
// int pad3:8; // ----------| total 1 byte ------------| total 4 bytes (int)
// int pad4:8; // padding total 23 bits
int bb;
int septet:7; // 7 bits
// int pad5:1; // 1 its
char dd;
// char pad6[2]; // 2 bytes
}; // length = 8 bytes
struct foo6 {
int a:5; // 5 bits
int b:3; // 8 bits
// int pad:24; // padding 24 bits
};
// total: 4 bytes
struct foo7 {
int a:30; // total 30 bits
// int pad1:2; // 填充2个
int b:3; // total 33 bits > 32 bits装不下,所以从新的字(int)开始
// int pad2:29;
};

由于内存对齐的存在,我们在设计结构体或者C++的类时(C++中的结构体和类也有内存对齐), 就需要细心了. 比如下面的foo8占用24bytes, 但是通过调整成员顺序得到foo9, 它的内存占用就变为了16bytes, 这是多么客观!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class foo8 {
char c;
// char pad1[7]; // padding 7 bytes aligning to p;
struct foo8 *p;
short x;
// char pad2[6]; // padding 6 bytes aligning to boundary
};
// sizeof(foo8) = 24
struct foo9 {
struct foo9 *p;
short x;
char c;
// char pad[5]; // padding 5 bytes aligning to boundary
};
// sizeof(foo8) = 16

详细的结构体内存对齐知识,可以研读失传的C结构体打包技艺,这是我从ludx那fork过来的,不是我翻译的,里面有原文链接, 另外clang编译器有个-Wpadded参数,加上它就会在编译输出信息中以警告的形式输出有关内存对齐的信息.

3. 结构体中的匿名成员

结构体中的匿名联合体或者结构体的成员,可以当做该结构体成员来访问,但是非匿名的就不能直接当成成员访问了.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
enum VarEnum {
T_EMPTY = 0,
T_DOUBLE = 1
}; // 4 bytes
struct VT_VARIENT {
enum VarEnum varType;
// char pad[4]; // 填充空间,浪费掉
union { // 结构体中的匿名联合体,可以当做结构体的成员来访问,
// 但是如果不是匿名的,则不能这样!!
double dbVal;
int nVal;
int boolVal:1;
};
}; // 16 bytes
struct ParamValue {
char name; // 10 bytes
char pad[7]; // 填充空间,浪费掉
struct VT_VARIENT value;
struct {
int i;
char ch;
};
}; // 24 bytes
int main(void)
{
// 结构体可以这样初始化,如果是按顺序来,可以不加.var这样的成员名字
struct ParamValue dstY = {.name = 'l', .value = {.varType = T_EMPTY, .nVal = 0}, .i = 100, .ch = 'A'};
printf("%d, %c\n", dstY.i, dstY.ch);
printf("%d\n", dstY.value.nVal);
dstY.value.nVal = 10;
printf("%d\n", dstY.value.nVal);
printf("sizeof(enum VarEnum) = %lu\n", sizeof(enum VarEnum));
printf("sizeof(struct VT_VARIENT) = %lu\n", sizeof(struct VT_VARIENT));
printf("sizeof(struct ParamValue) = %lu\n", sizeof(struct ParamValue));
printf("sizeof(double) = %lu\n", sizeof(double));
return 0;
}

4. union

  • 所有成员共享一块内存空间,同一时间只能存储其中的一个数据成员,所有的数据成员具有相同的起始地址(等于union的起始地址);
  • union所占字节数等于其最大成员所占字节数;
  • 用union检测大小端(big_endian,little_endian):
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    int IsLittleEndian()
    {
    union un_ {
    short sh;
    char ch;
    } check;
    check.sh = 0x01;
    if (check.ch == 0x01) {
    return 1;
    } else {
    return 0;
    }
    }

注: 大端模式和小端模式:
几乎所有的机器上,多字节对象都被存储为连续的字节序列,例如C语言中,int型变量x的地址为0x100,则&x=0x100,这个0x100是指int所占的四个字节中地址最低的那个字节的内存地址(基址),与内存增长顺序无关。也就是说,x的四个字节将被存储在存储器的0x100,0x101,0x102,0x103的位置
而存储地址内的排列则有两种通用规则:大端模式和小端模式
大端:低地址存高字节
小端:低地址存低字节

3. enum(枚举,是个好东西)

  • 枚举的成员称为枚举常量,枚举常量的值只能取整型值,不能是浮点值;
  • 枚举里面的成员是可以取到的值,跟int变量可以取… -2 -1 0 1 2 …一样, 所以sizeof一个enum的结果是4,因为默认成员是int类型;
  • 注意: struct,union成员都是用分号”;”分割,而enum是用逗号”,”分割的; struct,class,union,enum的{}后面要加”;”。
  • 用enum标示插件或者选项:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    enum flag_ {
    ATE = 0x00000001;
    APP = 0x00000002;
    IN = 0x00000004;
    OUT = 0x00000008;
    BIN = 0x00000016;
    TRUNC = 0x00000032;
    } flag;
    ...
    flag option = OUT | ATE | BIN;
    bool open(const char* filename, const flag option)
    {
    if (option & ATE == ATE) { // 设置了此选项
    ...
    }
    if (option & APP == APP) { // 设置了此选项
    ...
    }
    ...
    }
    // 同样的,插件系统中也可以用enum标示哪些插件启用了,哪些没启用,启用一个就按位或(|)上对应标识,关闭一个就按位与(&)上对应标识的按位取反(~)

控制语句

  • default语句不应省略,而且要用来处理真正的默认情况(或者未定义的行为);
  • case 后面只能是整型或字符型的常量或常量表达式,const变量不可以;
  • 多重循环中,尽可能将长循环放在外层,最短的放最内层;
  • 函数如果无参数,应当显示用void指明;
  • 所有函数都应该显示给出返回值,不应以来编译器特性(构造,析构除外).

typedef(给已经存在的类型起别名)

1. typedef与类型限定符const

1
2
3
4
5
6
7
8
9
typedef struct student
{
// Code
} student_st, *student_pst;
...
student_st stu1; // <=> struct student stu;
student_pst stu2; // <=> struct student *stu <=> student_st *stu;
const student_pst stu3; // => const stu3, 对于编译器来说,student_pst就是个类型说明符, 我们分析的时候直接去掉就行,跟int等一样.
student_pst const stu4; // => const stu4

2. typedef与#define

1
2
3
4
#define INT32 int
typedef long INT64;
unsigned INT32 i = 10; // no problem
unsigned INT64 j = 10; // 不行, typedef的类型不支持用类型修饰符进行扩展
1
2
3
4
#define PCHAR char*
typedef char* PCHAR2;
PCHAR p1, p2; // => char* p1, p2 => p2不是指针,是char变量
PCHAR2 p3, p4; // p3, p4都是指针,typedef定义的就是数据类型,与int等的地位用法是一样的.

sizeof

sizeof()求值是在编译期, 所以类似如下代码是没问题的:

1
2
int a[10] = {0};
size_t size_a = sizeof(a[15]); // <=> sizeof(int)

但是如果通过函数而不是sizeof()来使用a[15]就会出问题,因为函数是运行期执行的,运行期a[15]明越界了.

符号

注释(/**/ //)

1. /* */ 预处理时,编译器会使用空格替换,所以

1
in/*...*/t i = 0; // 是错误的, gcc -E 编译后变成 in t i = 0;

2. y = x / *p 不等于 y = x/*p,

因为编译器解析语法的时候,是从左到右,找最长的有意义的单元,对于第二段代码,编译器找到x后继续往后找,判断’x/‘是否是合法的标识符或者关键字,发现不是,于是记录下’x’,
然后从’/‘开始找,继续向下找,判断’/‘是否是有意义的符号,发现是,他是注释的开始,但不会停止,编译器继续往下找,判断’/\p’是不是有意义的符号,发现不是,于是记下’/*‘,
再从’p’开始, … ,
所以第二段语句是错误的,因为解析出来了’/*‘但没解析出*/,无法配对,所以报错.

逻辑运算符(|| &&)

1. 存在短路现象:

1
2
3
4
5
int i = 0, j = 0;
if ((++i > 0) || (++j > 0)) {
printf("i = %d, j = %d", i, j);
}
// i = 1, j = 0

或运算: 从左到右,发现值大于0的条件后,后面的条件不会再计算;
与运算: 从左到右,发现值等于0的条件后,后面的条件不会再计算。

位运算符(| & ~ ^ << >>)

  • << 左操作数的各个二进制为向左边移动右操作数个位值,高位丢弃,低位补零;
  • >> 左操作数的各个二进制为向右边移动右操作数个位值,无符号数,高位补零,有符号数,高位补符号位的数值(正数为0,负数为1);
  • << >> 的位数不能大于数据长度,不能小于0。

自增自减运算符(++ –)

1. C中的计算单位:

  • 逗号表达式中,每一个”逗号(,)”,都表示一个计算单位的结束;
  • for循环中,中间的条件表达式计算完成之后,表示一个计算单位结束;
  • “分号(;)”表示一个计算单位结束。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    int i = 1;
    int j = (i\++, i\++, i\++); // 逗号表达式,i每遇到一个逗号就认为本计算单元结束,自加
    int y;
    //每次y与10比较完,不管比较结果,Y就认为本计算单元结束,自加,不是{}里的算完就马上加。
    for ( y = 0; y < 10; y\++)
    {
    NULL;
    }
    i = 1;
    int k = (i\++) + (i\++) + (i\++); // i遇到;认为本次计算单元结束,把累积的三次自加全部执行一次,为6

2. var++,var–计算单位结束后才会进行自增或者自减操作.

运算符优先级

1. 运算符优先级表



2. 易错点:

(copy自C语言中文网)


op_tab

连接符(\)

反斜杠作为连接符时,后面不能有任何字符,即使空格也不行.

1
2
3
4
5
printf("This is \
perfect!\n");
// 连接符后面不能加空格。
// 连接符下一行中如果前面为了对齐使用了空格,空格会被当做有效字符。
// 输出: This is perfect!

预处理

预处理指令:

1. 预处理指令表

(copy自C语言中文网)



pre-process2

2. ASCII C 定义的宏:

_LINE_: 正在编译的文件的行号
_FILE_: 正在编译的文件名
_DATE_: 编译时刻的日期字符串 “25 Dec 2017”
_TIME_: 编译时刻的时间字符串 “12:30:58”

宏定义(#define #undef)

1
2
3
4
#define SUM(x) ((x)+(x)) // no problem
#define SUM (x) ((x)+(x)) // 错误,SUM后不该有空格
#define TOCHAR(x) #x // # -> 转化为字符
#define SQR(x,y) x##y // ## -> 合并

#pragma预处理

用于设定编译器状态或者只是编译器完成一些特定动作

1. #pragma message(“文本”)

在编译信息输出窗口输出相应的”文本”信息.

2. #pragma once

保证头文件只被变编译一次,防止头文件的重复包含.

3. #pragma warning(…)

1
2
3
4
#pragma warning(disable:405034; once: 4366; error: 164)
// disable: 不显示对应的警告
// once: 对应警告只显示一次
// error: 将对应警告当做错误处理

4. #pragma pack(n)

1
2
3
#pragma pack(8) // 设置字节对齐方式位8字节对齐
...
#pragma pack() // 取消自定义的字节对齐方式

注: 这里用

pack(8)```设置的8,只是一个最大值,实际对齐的字节数为:min(成员中所占字节最大值, 8):
1
2
3
4
5
6
7
8
9
```C
struct Test
{
int i; // 4 bytes
char [2]; // 2 bytes
// char pad[2]; // 编译器填充两个字节
float f; // 4 bytes
}
// 该结构体的大小位12bytes, 它是4字节对齐,因为min(4,8)=4.

指针与数组

指针

指针就是一个变量, 只不过这个变量的值,是一个内存地址而已.

  • 指针变量 加/减 整数,所表达的含义是:相对当前指针值的偏移,偏移量为: 整数*指针指向的数据类型的大小
  • void 类型的指针,偏移时是以字节为单位的,同char (gcc)
  • 相同类型的指针 相减,所表达的含义是: 这两个指针之间的偏移量(元素的个数) <=>
    / sizeof(data_type)```
    1
    2
    3
    4
    5
    6
    7
    8
    9
    * 相同类型的指针不能进行 相加 运算
    * 不同类型的指针不能进行算数运算
    ### 函数指针
    #### 1. 函数指针
    函数与变量/常量一样,也要存储与内存中,只不过变量/常量存的是数据,而函数存的是逻辑或者叫算法,之前说过,凡是内存中的东西,都可以通过指针访问(当然这只是理论上),那函数也不例外,也可以通过指针来访问,指向函数的指针成为函数指针,函数名称就代表函数的地址,当然也可以对函数名取地址,二者效果相同, 即```func() => func <=> &func```.
    ```C
    char *func(const char* ch1, const char* ch2); // 声明一个函数
    char (*pfunc)(const char* ch1, const char* ch2); // 定义一个函数指针

上面的代码中,func是一个返回字符指针的函数, 而pfunc是一个返回字符的函数指针,二者的形参列表是相同的.

2. 函数指针数组

1
2
3
4
5
#define N 10
char* pdata[N];
char* (*pfunc[N])(const char *p); // <=> char* (*)(const char *p) pfunc[N];
// |-----------------------|
函数指针类型

上面的代码定义了一个函数指针数组,跟指针数组没什么区别,只不过指针数组里的元素指向的是数据,而函数指针数组里面的元素指向的是函数而已,但就数组本身而言,二者是完全一样的,N一定,二者大小都一样.

既然是数组,当然也可以定义函数指针数组的指针, 与普通数组指针没啥区别:

1
2
3
#define N 10
char* (*pdata)[N];
char* (*(*pfunc)[N])(const char *p); // <=> char* (*)(const char *p) (*pfunc)[N];

数组

1. 数组名的含义

数组名不能作为左值(放在赋值运算符左边), 作为右值时代表的是数组首元素地址, 但sizeof(数组名)的结果是整个数组的大小,因为此处数组名不是右值;
如下例中, a是数组名(代表数组首元素的地址), 而&a的含义是数组的地址, 它们在值上是相等的,因为数组首元素相对于整个数组来说,它的偏移是0,所以在内存中数组的地址等于数组首元素的地址; 但是它们两个的含义完全不同,通过取值就能看出, (&a) 是地址,等同与数组名a, 而a是一个值,是首元素的值.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
int a[5] = {1, 2, 3, 4, 5};
printf("a = %p\n", (void*)(a));
printf("&a[0] = %p\n", (void*)(&a[0]));
printf("&a = %p\n", (void*)&a);
printf("*a = %d\n", *a);
printf("*&a[0] = %d\n", *&a[0]);
// 返回的是地址i,其值与&a[0]、a相同,但含义不同,代表整个数组的首地址
printf("*&a = %p\n", (void*)(*&a));
printf("sizeof(*a) = %lu\n", sizeof(*a));
printf("sizeof(*&a[0]) = %lu\n", sizeof(*&a[0]));
// &a代表的是整个数组的首地址,作为右值代表的数组首元素的地址\n
printf("sizeof(*&a) = %lu", sizeof(*&a));
*a = 11;
printf("a[0] = %d\n", a[0]);
// OUT :
// a = 0x7ffc336df2a0
// &a[0] = 0x7ffc336df2a0
// &a = 0x7ffc336df2a0
//
// *a = 1
// *&a[0] = 1
// *&a = 0x7ffc336df2a0
//
// sizeof(*a) = 4
// sizeof(*&a[0]) = 4
// sizeof(*&a) = 20a[0] = 11

2. 数组与指针

它们没有任何关系, 只是有相似的使用方式而已.
虽然前面我们说数组名代表首元素的地址, 但只是代表而已, 实际上并没有a这么一个变量,里面存着数组有元素的地址.

1
2
3
4
5
6
7
8
9
10
char a[] = "pointer vs array";
char *p = "pointer vs array";
printf("%lu, %lu\n", sizeof(a), sizeof(p));
printf("%lu\n", sizeof(&a));
printf("%lu, %lu\n", sizeof(*(&a)), sizeof(*p));
// OUT:
// 17, 8
// 8
// 17, 8

由上例可以看出, a显然不是指针,因为指针变量的大小是8, 而&a才是实实在在的指向数组的指针;
另外(&a)与p也是完全不同的, (&a)表示的是整个数组, 而p只是代表一个字符而已;
只要是存在与内存中的数据,都能通过指针来访问,数组也是存在于内存中的数据,所以它也可以通过指针(利用基址+偏移地址方式)来访问, 就跟用指针访问基本数据一样, 只是恰好C语言支持指针通过下表来操作, 同时数组支持通过偏移的形式来操作, 所以看起来指针与数组比较像而已.

1
2
3
4
char a[] = "pointer vs array";
char *p = "pointer vs array";
char tmp1 = a[0]; // <=> *((char*)&a + 0)
char tmp2 = p[0]; // <=> *(p + 0)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
-------栈--------|------|-----堆/静态区---------
|-----| | |
|-----| | |
a | ... | | |
|-----| | |
|-----| | |
|-----| | |
| |
| p |------|------|---->|-----|
| | |-----|
| | | ... |
| | |-----|
| | |-----|
| | |-----|
-----------------|------|-----------------------

编译器总是把基于下标的操作解析为基于’基址+偏移’的操作方式, 汇编就是这么干的.

3. 基址+偏移方式访问数组元素问题

直接看下面的例子即可, ptr3这种情况需要特别注意:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int a[5] = {1, 2, 3, 4, 5};
// &a指向数组的指针,+1就是偏移一个数组的大小,现在它指向a[5]下面紧邻的那个
// 5×sizeof(int)的区域(未定义区域)。本来是指向数组的指针,值为数组首元素
// 的地址,强制转换成int*后就是指向一个int值的指针了,此时*ptr1是一个int值
int *ptr1 = (int *)(&a + 1);
// a是数组名,指向数组首元素,是一个int*,对它加1,就是向后偏移sizeof(int)
// 个字节,指向数组第二个元素,其实这里的强制类型转换是多余的,但写上会使概念更清晰。
int *ptr2 = (int*)((int*)a + 1); // <=> a + 1
// 转换成标量, +4就是+4bytes, 指向第二个元素
int *ptr3 = (int*)((unsigned long)a + 4);
// ptr1是一个指向int的指针, 它目前指向数组a最后一个元素后面紧跟的那个地址,
// ptr1[-1]就是向前偏移sizeof(int)bytes, 也就是a的最后一个元素.
printf("%x, %x, %x\n", ptr1[-1], *ptr2, *ptr3);
// OUT
// 5, 2, 2

4. 数组指针与指针数组

(\*p)[]``` => 数组指针, p是指向数组的指针, 有一点需要注意, 此时\*p跟数组名同义, 代表数组首元素地址,所以\*\*p就是一个int型变量,大小为4bytes;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
```int \*p[]``` => []小标运算符的优先级高于\*, 所以等价于int\* (p[]), 首先p是一个数组, 数组元素的类型是int\*, 即指针数组.
```C
int a[5] = {1, 2, 3, 4, 5};
// 指针数组与数组指针
int (*p)[5] = &a; // p是个指针
// pp是一个数组,数组元素是int型指针
int *pp[5] = {NULL, NULL, NULL, NULL, NULL};
// p是指针,大小是8bytes
printf("sizeof(p) = %lu\n", sizeof(p));
// *p是p所指向的数组,它等效于数组名
printf("sizeof(*p) = %lu\n", sizeof(*p));
// **p是p所指向数组的第一个元素4bytes
printf("sizeof(**p) = %lu\n", sizeof(**p));
printf("sizeof(pp) = %lu\n", sizeof(pp));
printf("sizeof(*pp) = %lu\n\n", sizeof(*pp));
// 等效于数组名
printf("*p = %p\n", (void *)(*p));
// 数组名代表数组首元素的地址,这里只是偏移sizeof(int)
printf("*p+1 = %p\n\n", (void *)(*p+1));
// OUT:
// sizeof(p) = 8
// sizeof(*p) = 20
// sizeof(**p) = 4
//
// sizeof(pp) = 40
// sizeof(*pp) = 8
//
// *p = 0x7fff90402500
// *p+1 = 0x7fff90402504

5. 数组作为函数参数

一维数组作为函数参数, 编译器会把它处理成指针:

1
2
3
4
void Print(char text[])
{
printf("%lu\n", sizeof(text)); // 输出始终为8, 不论传什么进来
}

6. 多维数组

一维数组作为函数参数, 编译器会把它处理成指针, 但这个过程不是递归的, 也就是说只有一维数组才会这样, 当数组超过一维时, 将第一维改写为指向数组的指针后,后面的维再也不可改写:

1
2
3
4
5
6
7
void func1(char a[][4]) {
// code
}
// 两个函数等价
void func2(char (*p)[4]) {
// code
}

二维数组做参数第一维的维度可以省略,但第二维的不行, 因为它标示了第一维的指针指向数据的类型.
有个小陷阱要注意:

1
2
3
4
5
6
7
8
9
10
int a[3][2] = {{0, 1}, {2, 3}, {4, 5}}; // 二维数组赋值
int b[3][2] = {(0, 1), (2, 3), (4, 5)}; // 注意里面不是中括号是逗号表达式 <=> {1, 3, 5}
int *p1, *p2;
p1= a[0];
p2 = b[0];
printf("%d, %d\n", p1[0], p2[0]);
// OUT:
// 0, 1

内存管理

linux程序内存模型:

每一个进程都有一个大小与物理内存相同的虚拟内存空间, 然后具体用时映射到物理内存, 因为有虚拟内存空间的存在, 所以编译器和连接器可以在编译或链接时直接分配内存地址, 它们分配的是虚拟内存地址.



  • 用户栈: 局部自动变量,函数栈等,向低地址生长
  • 运行时堆(动态内存分配区): malloc/new, 向高地址生长
  • 读/写段(静态数据区):
    • .data: 已经初始化的全局自动变量和静态变量(全局的和局部的)
    • .bss: 未初始化的全局自动变量和静态变量(全局的和局部的)
  • 只读段(代码段):
    • .text: 存放程序代码
    • .rodata: 常量区,存储字符串常量,const常量(全局的或者静态的,局部的不在这)
      所谓的内存管理,其实就是管理”运行时堆”.

函数

递归

不使用库函数编写strlen, 但是当str很长时间,这个函数并不实用:

1
2
3
4
5
6
7
8
size_t myStrlen(const char *str)
{
if (*str != '\0') {
return (myStrlen(++str) + 1);
} else {
return 0;
}
}

StarrySky

StarrySky

1 日志
1 标签
RSS
GitHub ZhiHu
© 2015 - 2017 StarrySky