再读C++的设计艺术(4)

Posted by Johan Niu on April 15, 2018

再读C++的设计艺术(4)

1 先声明后使用原则

声明(声明变量和函数)是给编译器看的。

定义变量时才会开辟内存,声明不会开辟,声明是个编译器看的,如果声明了一个变量, 且后面没有使用,则编译器会将其优化,视为不存在,不会开辟内存。

写一个函数的顺序是先声明,再定义,最后调用。

但是声明和定义可以合起来写。例如下面的写法:

void Swap(int, int);
void main() {
    Swap(a ,b);
}
void Swap(int x, int y)  {
    int temp = x;
    ...
}

首先声明函数Swap是为了给编译器看,知道了其返回值类型和参数类型几个数,只是为了计算其在 栈中占用空间,方便栈指针跳转等(不得而知),所以不用写参数名字,有了其类型就够了。

而定义的时候,是必须要知道参数的名字的,第一因为函数中要用到这个参数。

但是问题来了,如果不写参数名字,调用函数时形参在从左到右一个一个的进栈时,由此每个参数在栈中的内存地址我都知道了,函数中的使用不就可以直接访问那个地址了吗?这也就导致了第二个原因,函数参数进栈的顺序是不一定的,所以必须为其加上名字,后面才能准确拿到参数。

2 函数参数进栈顺序

首先,参数入栈顺序为:从右向左

原因:是为了在使用C/C++的“函数参数长度可变”这个特性时更方便。

如果是从左向右压栈,第一个参数将被放在栈底,最后一个可变参数放在栈顶,由于可变,第一个栈顶的参数取起来特别麻烦。

从右向左,最后一个可变参数放在栈底,第一个放在栈顶,可以快速方便取出第一个参数。

3 ++a 与a++

如下例子:

#include <cstdio>

int main() {
    int a = 10;
    printf("%d %d %d\n", a++, ++a, a);
    return 0;
}

按照从右向左读取,想当然的结果应该是:11 11 10,执行后a = 12。

但是看结果发现结果是:11,12,12

这是为什么呢?

原因在于:

1. 在将参数入栈前,编译器会先把参数的的表达式都处理掉,哪怕这些运算会改变其中某些参数的值,

2. ++a 是直接在变量a地址所指的值,直接加1;

3. a++ 是先取变量a地址所指的值,将该值备份到缓冲区,然后再对a地址所指的值加1**

看如下例子:

#include<stdio.h>

int main() {
    int a = 11;
    printf("%d  %d %d %d %d %d\n", ++a, a++, a++, ++a, a, a++);
    return 0;
}

猜想如下:

a++缓冲区 a
11 12
11 12
11 13
13 14
14 15
14 16

16,14,13,16,16,11

实际输出结果为:

16  14 13 16 16 11

猜想正确。

引用与参考:

https://www.cnblogs.com/easonliu/p/4224120.html

https://blog.csdn.net/hnyzyty/article/details/46427219

4 地址重定位

例如在main.c中调用了a.h中的函数,a.h中的函数只声明而不定义,所以在main.obj中,也就是在main.c编译之后的机器码中,IP跳转到该函数的地址是不知道的,因为该函数的.obj还没有链接到一起,所以这些调用函数地方指针都空出来了,当所有obj都拼到一起之后,编译器算出该函数地址的值赋值给原来指针的地方,也就是地址重定位。

5 程序打包

一个.cpp文件编译之后一般来说生成一个.obj文件(目标文件,机器码,可执行,但不是立即可执行)。然后将多个编译生成的.obj文件以及标准库等其他的.obj文件拼在一起后,然后进行地址重定位,将其之间的关系全部联系起来。最后加入一些头等打包成exe。

6 静态链接与动态链接

所谓lib库等,就是将一大堆的.obj文件合起来,然后加入一些引导头之类的。

上述讲的这些链接,都是静态链接,链接之后还是在硬盘中,执行的时候加载到内存中。

例如printf这个函数的代码,在运行之前已经存在于exe,这就是静态链接。

相对的就是动态链接dll。

链接printf的时候,不是讲”stdio.h”这个库连接进去,而是将printf链接进去,以最简化原则, 需要谁连接谁。

静态链接中,例如连续调用多次printf函数,实际上就链接了一次,只产生一份代码,只是传的参数不同。

但是站在操作系统的角度来看,系统中有多个程序在执行,每个程序都调用了printf,也就是每个程序都 链接了一次printf。站在程序的角度看,自己已经很节约了只链接了一次,但是站在操作系统的角度看, 链接了多次,产生了浪费,其实只要搞出一份代码就可以,自己的程序访问别人程序的代码,但是这样就导致了跨进程的问题,十分不安全。而动态链接库就解决了这个问题。动态链接库没有将代码放到某个程序中,而是放到操作系统中,当某个程序使用其的时候,应用loadLibrary这个技术,加载该库。

如果要用printf函数,就从dll中找到该函数的地址。原来是静态的算好printf函数的地址,现在变成了动态获取printf函数的地址。

7 作用域与生存周期。

static局部变量,全局的生存周期,局部的作用域。