C语言:程序环境和预处理

news2024/11/25 19:32:48

编译一个C程序设计很多步骤,大致为预处理,编译,汇编和链接.

在讲解预处理阶段之前,先简单总述一下程序的编译和链接.

1. 程序的编译和链接

链接是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可被加载(复制)到内存并执行.

这里有两个源文件构成了一个程序

  • test.c
int sum (int *a, int n);

int array[2] = {1, 2};

int main()
{
    int val = sum(array, 2);
    return val;
}
  • sum.c
int sum(int *a, int n)
{
    int i, s = 0;

    for (i = 0; i < n; i++)
    {
        s += a[i];
    }
    return s;
}

若我想将这两个源文件最终形成一个可执行程序,就需要通过编译系统提供的编译器驱动程序来处理, 它代表用户在需要时调用语言预处理器,编译器,汇编器和链接器.

例如:
visual studio 使用的就是 MSVC 编译器
GNU 编译系统要构造实例程序,就要通过在 shell 输入指令调用 GCC 驱动程序

我在 shell 输入以下指令来构建目标程序:
在这里插入图片描述

随后在我的 build 文件夹中,可执行程序prog就在里面
在这里插入图片描述

这其中经历了如下过程:
在这里插入图片描述

若分成预处理,编译,汇编和链接这四个步骤,指令如下:
预处理
在这里插入图片描述

驱动程序首先运行C预处理器(cpp),它将C的源程序test.c,sum.c翻译成一个
ASCII码的中间件test.i,sum.i

编译
在这里插入图片描述

接下来,驱动程序运行C编译器(cc1),它将test.i,sum.i翻译成一个ASCII汇编语言文件test.s,sum.s
现版本GCC使用 cc1 可以完成 预处理 和 编译 两个步骤.
在这里插入图片描述

汇编
在这里插入图片描述

然后,启动程序运行(as),它将test.s,sum.s翻译成一个可重定位目标文件test.o,sum.o
在这里插入图片描述

链接
在这里插入图片描述

最后运行链接器ld, 将test.osum.o预计一些必要的系统目标文件组合起来,创建一个可执行目标文件prog
这里显示的collect2ld的封装,最终还是要调用ld来进行链接的
在这里插入图片描述

最后执行prog
在这里插入图片描述

shell调用操作系统中一个叫做加载器(loader)的函数,它将可执行文件prog中的代码和数据复制到内存,然后将控制转移到这个程序的开头.

在这里插入图片描述


在x86-64 Linux 环境下, 可重定位目标文件是可执行可链接(ELF)格式的,大致如下:
在这里插入图片描述

每个可重定位目标文件有一个节叫做.symtab,这是符号表,用来存放每一个符号的信息的, 符号包括 函数, 全局变量或者静态变量.

使用readlf指令可以看到test.osum.o的符号表

  • test.o的符号表
    在这里插入图片描述

  • sum.o的符号表
    在这里插入图片描述

链接所做的就是对符号进行解析和重定位(将段表和符号表进行类似合并的操作),最后生成可执行文件.

在这里插入图片描述

2. 预处理详解

C预处理器(preprocessor)在源代码编译之前对其进行一些文本性质的操作.

它的主要任务包括删除注释,插入被#include指令包含的文件的内容,定义和替换由#define指令定义的符号,以及确定代码的部分内容是否应该根据一些条件编译指令进行编译.

2.1 预定义符号

下面有一些由预处理器预定的符号.
有助于调试,添加版本信息和结合条件编译

符号示例值含义
__FILE__“name.c”进行编译的源文件名
__LINE__25文件当前行的行号
__DATE__“Jan 31 1997”文件被编译的日期
__TIME__“18:00:00”文件被编译的时间
__STDC__1如果编译器遵循 ANSI C, 其值就为 1 , 否则未定义

下面我在x86-64Linux环境下运行

#include<stdio.h>

int main()
{
	printf("%s\n", __FILE__);
	printf("%d\n", __LINE__);
	printf("%s\n", __DATE__);
	printf("%s\n", __TIME__);
    printf("%d\n", __STDC__);

	return 0;
}

程序运行结果如下:

在这里插入图片描述

2.2 #define

下面是#define的正式用途

#define name stuff

有了这条指令后,源文件每次出现name,预处理阶段就会将其替换成stuff

替换文本不仅局限于数值字面值常量,使用#define指令,可以把任何文本替换到程序中,例如:

#include <stdio.h>

#define N 100
#define do_forever for(;;)
#define CASE break;case

int main(void)
{
    int a = N;
    printf("%d", a);

    do_forever;

    switch(a)
    {
        case 1:
        CASE 2:
        CASE 3:
    }

    return 0;
}
  • #define N 100将 100 命名为 N
  • #define do_forever for(;;)将更具代表性的符号来命名无限循环的for语句`
  • #define CASE break;case省去了需要写break的情况,但不推荐使用,对代码可读性有明显降低

我通过将test.c预处理生成test.i
在这里插入图片描述

打开test.i发现,文件上面多出了很多行代码,这些都是<stdio.h>头文件中的代码,定位到main函数发现:通过#define命令命名的符号都被替换成了对应数值:

在这里插入图片描述


如果定义的 stuff 过长,可以使用反斜杠\来进行换行

#include<stdio.h>

#define DEBUG_PRINT printf("File %s line %d:"\
                        "x = %d, y = %d, z = %d",\
                        __FILE__, __LINE__,\
                        x, y, z)

int main(void)                    
{
    int x = 1;
    int y = 2;
    int z = 3;

    x *= 2;
    y += x;
    z = x * y;
    DEBUG_PRINT;

    return 0;
}

这里利用了相邻字符串常量被自动连接成为一个字符串的特性
程序运行结果如下:
在这里插入图片描述

注意:最好不要在宏定义末尾添加逗号;,如果在使用if-else判断会出错

if(...)
    DEBUG_PRINT;    //若宏定义尾部添加了;  则在这会有两个逗号
else                //else没有相对应的if匹配,程序出错        
    ...

当然宏定义也可以直接定义一序列语句,例如函数,循环语句

#define SUM_LOOP                \
        for (i = 0; i < 10; i++)\
        {                       \
            sum += i;           \
            if (i > 0)          \
                prod *= i;      \
        }    

但不推荐这样,如果一串长代码经常出现在很多地方,应该是把这段代码放入函数而不是通过#define


2.2.1 宏

#define机制包括一个规定,允许把参数替换在文本中,这种实现成为或定义宏(defined macro)

#define name(parameter-list) stuff
  • name(parameter-list)之间不能有空格,如果有空格,(parameter-list)也会成为stuff的一部分
  • parameter-list是一个用逗号分隔的参数列表,它们可能出现在stuff
  • 当宏被调用时, 每个参数对应的实际值将被替换到stuff

例如:我定义了一个计算平方值的宏

#include<stdio.h>

#define SQUARE(x) x * x

int main(void)
{
    int x = 5;

    printf("%d\n", SQUARE(x));
    printf("%d\n", SQUARE(x + 1));

    return 0;
}

但是程序运行结果如下:
在这里插入图片描述

为什么printf("%d\n", SQUARE(x + 1));得到的结果不是 36 而是 21 呢?

通过预处理指令得到test.i并打开后看到:

在这里插入图片描述

(x + 1)并没有先计算再替换宏,而是原封不动的替换进去,在进行计算.
因为宏是在预处理阶段进行替换的,而x的值则是在实际运行阶段,开辟栈空间创建赋值的,谁先谁后一目了然.

在宏定义添加两个括号就可以解决这个问题:

#define SQUARE(x) (x) * (x)

这样SQUARE(x + 1)得到的就是(x + 1) * (x + 1)


这里有另外一个宏定义

#include<stdio.h>

#define DOUBLE(x) (x) + (x)

int main(void)
{
    int x = 5;

    printf("%d\n", DOUBLE(x + 1));
    printf("%d\n", 10 * DOUBLE(x + 1));
    return 0;
}

程序运行结果如下:
在这里插入图片描述

printf("%d\n", 10 * DOUBLE(x + 1));没有是我预期的12而是66
再次观察预处理后的文本:
在这里插入图片描述

还是运算优先的问题

对其进行修改

#define DOUBLE(x) ((x) + (x))

这样DOUBLE(x + 1)得到的就是((x + 1) + (x + 1))

注意:事实上,所有对数值表达式求值都应该用上面的方式加上括号,避免因为操作符优先而造成不可预料的结果.

2.2.2 #define替换

在程序中扩展#define定义符号和宏时, 需要涉及几个步骤

  1. 在调用宏时, 首先对参数进行检查, 看看是否包含了任何由#define定义的符号. 如果是, 它们首先被替换.
  2. 替换文本随后被插入到程序中原来的位置. 对于宏, 参数名被他们的值所替代
  3. 最后, 再次对结果文本进行扫描, 看看它是否包含了任何由#define定义的符号.如果是, 就重复上述处理过程.

这样,宏参数和#define定义可以包含其他#define定义的符号.例如

#define N 100
#define INC(x) ((x) + 1)

int x = INC(N);           //x = 101

在这里插入图片描述

但是,宏中不可以出现递归.


当预处理器搜索#define定义的符号时,并不检查字符串常量的内容.

  • 例如:
#include<stdio.h>

#define N 100

int main(void)
{
    printf("N = %d", N);

    return 0;
}

程序运行结果如下:
在这里插入图片描述

预处理后文本也确实没有对常量字符串内的符号进行替换:

在这里插入图片描述

  • 还比如:
#include<stdio.h>

#define PRINT(x) printf("The value of x is %d\n", x)

int main(void)
{
    int x = 2;
    int y = 3;
    double z = 1.2;

    PRINT(x);
    PRINT(y);
    PRINT(z);

    return 0;
}

程序运行结果如下:
在这里插入图片描述

也只能限制整数的形式
预处理后文本也确实没有对常量字符串内的符号进行替换:
在这里插入图片描述


那么,如果想把宏参数插入到字符串常量中,可以使用两个技巧

  • 第一个技巧,利用邻近字符串自动连接的特性
#include<stdio.h>

#define PRINT(VALUE, FORMAT) printf("The value is "FORMAT"\n", VALUE)

int main(void)
{
    int x = 2;
    int y = 3;
    double z = 1.2;

    PRINT(x, "%d");
    PRINT(y, "%d");
    PRINT(z, "%f");

    return 0;
}

程序运行结果如下:
在这里插入图片描述

预处理后文本如下:
在这里插入图片描述

但是这样只有当字符串常量作为宏参数才可以使用

  • 第二个技巧,使用预处理器把一个宏参数转换为一个字符串.

#argument这种结构被预处理器翻译为"argument"

这样我可以更好的更改我的PRINT

#include<stdio.h>

#define PRINT(VALUE, FORMAT)            \
        printf("The value of "#VALUE    \
        " is "FORMAT"\n", VALUE)

int main(void)
{
    int x = 2;
    int y = 3;
    double z = 1.2;

    PRINT(x, "%d");
    PRINT(y, "%d");
    PRINT(z, "%f");

    return 0;
}

程序运行结果如下:
在这里插入图片描述

预处理后文本如下:
在这里插入图片描述


##结构则执行一种不同的任务

它把位于自己两边的符号连接成一个符号.
它允许宏定义从分离的文本片段创建表示符.

例如:

#define CAT(x, y) x##y

int main(void)
{
    int VB12 = 13;

    printf("%d\n", CAT(VB, 12));

    return 0;
}

程序运行结果如下:
在这里插入图片描述

预处理后文本如下:
在这里插入图片描述

注意:连接之后的标识符必须合法!

2.2.3 带副作用的宏参数

当宏参数在宏定义中出现的次数超过一次时,如果这个参数具有副作用,那么在使用这个宏时就可能出现危险,导致不可预料的结果.

副作用就是表达式在求值时出现永久性的效果
例如:

x + 1;

这个表达式可以执行上百次,得到的结果每次都是一致的,x的值仍然不变.这个表达式不具有副作用.

但是:

x++;

就具有副作用了.当这个表达式下一次执行时,x的值就发生了改变.


观察下列代码,它会打印出什么呢?

#include <stdio.h>

#define MAX(a, b) ((a) > (b) ? (a) : (b))

int main(void)
{
    int x = 5;
    int y = 8;
    int z = MAX(x++, y++);

    printf("x = %d, y = %d, z = %d\n", x, y, z);
    return 0;
}

程序执行结果如下:
在这里插入图片描述

通过查看预处理后文件就可以知道了:
在这里插入图片描述

  • 第一步: (x++) > (y++),比较xy的大小,表达式为假,再对xy进行++操作,x6, y9
  • 第二步, 因为前面的表达式为假,执行z = y++,先执行z = y,z9;再执行++,y10
  • 最后x6,y10,z9

2.2.4 宏与函数

宏非常频繁的用于执行简单的运算,比如在两个表达式中寻找其中较大(或较小)的一个:

#define MAX(a, b) ((a) > (b) ? (a) : (b))

如果用函数完成这个任务呢?

int max(int a, int b)
{
    return (a > b ? a : b);
}

答案肯定是函数方法更消耗时间,调用函数会先开辟栈空间,并复制实参,再进行函数内部操作,最后再返回值,归还栈空间.
而宏只需要简单的在预处理阶段就进行进行文本替换.


通过对下面的代码生成汇编代码可以明显看出区别

#include <stdio.h>

#define MAX(a, b) ((a) > (b) ? (a) : (b))

int max(int a, int b)
{
    return (a > b ? a : b);
}

int main(void)
{
    int a = 3; 
    int b = 2;
    int c = 0;

    c = MAX(a, b);
    c = max(a, b);

    return 0;
}

通过调试进行反汇编,明显函数的代码要比宏的代码要多
在这里插入图片描述


宏相比函数的优点:

  • 用于调用函数和从函数返回的代码可能比实际执行这个小型计算所需要的时间
    更多.
    所以宏比函数在程序的规模和速度方面更胜一筹
  • 更为重要的是函数的参数必须声明为特定的类型.
    所以函数只能在类型合适的表达式上使用.反之这个宏可以适用于整形,长整型,浮点型等可以用来比较的类型.
    宏是类型无关的

当然宏也有缺点:

  • 每次使用宏的时候,一份宏定义的代码将插入到程序中.除非宏比较短,否则可能大幅增加程序的长度.
  • 宏是没法调试的
  • 宏由于与类型无关,也就不够严谨
  • 宏可能会带来运算符优先级的问题,导致程序容易出错

宏有时候也可以做到函数做不到的事情

宏的参数可以出现类型,而函数做不到

#include<stdio.h>

#define MALLOC(n, type) (type*)malloc(sizeof(type) * n)

int main(void)
{
    int* p = MALLOC(10, int);

    free(p);

    return 0;
}

可以看到预处理后的文件直接替换成对应符号:
在这里插入图片描述


下面是#define宏和真正的函数相比存在的一些不同的地方

属性#define函数
代码长度每次使用时,宏代码都被插入到程序中.除了非常小的宏,程序的长度将大幅增长函数代码只出现在一个地方;每次使用这个函数,都会调用在那个地方的同一块代码
执行速度更快存在函数调用/返回的额外开销
操作符优先级宏参数的求值是在所有周围表达式的上下文环境里,除非它们加上括号,否则邻近操作符的优先级可能会产生不可预料的结果函数参数只在函数调用时求值一次,它的结果值传递给函数.表达式的求值结果更容易预测
参数求值参数每次用于宏定义时,它们都将重新进行求值.由于多次求值,具有副作用的参数可能会产生不可预料的结果参数在函数被调用前只求值一次.在函数中多次使用参数并不会导致多个求值过程.参数的副作用并不会造成任何特殊的问题
参数类型宏与类型无关.只要对参数的操作是合法的,它可以适用于任何参数类型函数的参数是与类型有关的.如果参数的类型不同,就需要不同的函数,即使它们执行的任务是相同的

在C99标准下,有内联函数将宏和函数的优点都结合在了一起,后面会详细讲解的

2.2.5 命名约定

但是语言本身并不能让程序员区分#define宏和函数,就需要规定命名格式来方便区分.

一种常见的约定就是:

宏名字全部大写

这样也可以提醒程序员使用宏之前,注意传入有副作用的参数

但是有时候,库中会故意将宏设置成小写伪装成一个函数,例如offset

2.2.6 #undef

undef用于移除一个宏定义

#undef name 

如果一个现存的名字需要被重新定义, 那么首先必须用undef移除它的旧定义.

#include <stdio.h>

#define N 100

int main(void)
{
    printf("%d\n", N);
    #undef N
    printf("%d\n", N);
    return 0;
}

我先宏定义了N为 100 ,随后移除了N的定义, 打开预处理后的文件如下:
在这里插入图片描述

移除定义后, N没有被替换为 100

2.2.7 命令行定义

许多C编译器提供了一种能力, 允许在命令行定义符号,用于启动编译过程.

当根据同一个源文件编译一个按程序的不同版本时,这个特性是很有用的.例如:
假定某个程序声明了一个某种长度的数组.如果某个机器的内存很有限,这个数组必须很小,但在另一个内存充裕的机器上,你可以希望数组能够大一些

#include <stdio.h>

int main(void)
{
    int arr[SIZE] = {0,};
    int i = 0;

    for (i = 0; i < SIZE; i++)
    {
        arr[i] = i;
    }

    for (i = 0; i < SIZE; i++)
    {
        printf("%d ", arr[i]);
    }

    printf("\n");

    return 0;
}

上述代码,我没有规定SIZE的值,可以在命令行执行SIZE的值

GCC编译器下,-D选项可以完成这项任务

-Dname
-Dname=stuff

第一种形式定义了符号name,它的值为1;
第二种形式定义了符号name,定义为等号后面的值stuff

对于上面的程序,我在命令行对其进行编译链接:
在这里插入图片描述

运行可执行文件,得到如下结果:
在这里插入图片描述

SIZE定义为另外的值进行编译链接:
在这里插入图片描述

运行可执行文件,得到如下结果:
在这里插入图片描述


同样,在命令行也可以去除符号的定义,-U完成这项任务.
-Uname将导致程序中符号name的初始定义被忽略.当它与条件编译结合使用时,这个特性还是很有用的.

2.3 条件编译

在编译一个程序的时候我们可以使用条件编译将一条语句(一组语句)编译或者放弃.

使用条件编译,可以选择代码的一部分是被正常编译还是完全忽略.
用于支持条件编译的基本结构是#if指令和与其匹配的#endif指令.

单分支条件编译

#if constant-expression
        statements
#endif

其中constant-expression常量表达式由预处理器对其进行求值.如果它的值是非零值(真),那么statements部分就会被正常编译,否则预处理器就静默地删除它们.

常量表达式,需要是常量表达式或者是#define定义的符号.
如果在程序执行前都不能得到它的值,那么它在常量表达式中就是非法的,因为它们的值在编译时是不可预测的.


例如,我可以通过条件编译保留调试性代码,这样在我需要对程序进行调试的时候,就可以选择性的进行编译

#include <stdio.h>

#define DEBUG 1

int main(void)
{
    int i = 0;
    int arr[10] = {0,};

    for (i = 0; i < 10; i++)
    {
        arr[i] = i;
        #if DEBUG
            printf("%d\n", arr[i]);
        #endif
    }

    return 0;
}

DEBUG的值为真,则程序运行结果如下:
在这里插入图片描述

DEBUG的值为假,(将DEBUG的值设置为 0 ),程序没有进行任何输出:
在这里插入图片描述


多分支条件编译

条件编译也可以在编译时选择不同的代码部分.
为了支持这个功能,#if指令还有可选的#elif#else子句

#if constant-expression
        statements
#elif constant-expression
        other statements
#else
        other statements
#endif

#elif出现的次数不限,只有对应的分支的constant-expression为真,才会编译对应语句,如果都为假,则只会编译#else对应的语句

多分支条件编译可以用于一个程序有不同的版本,这样避免了为每个版本编写一组不同的源文件.

#include <stdio.h>

#define version1 0
#define version2 1
#define version3 0

int main(void)
{
    #if version1
        printf("use version1.\n");
    #elif version2
        printf("use version2.\n");
    #elif version3
        printf("use version3.\n");
    #else 
        printf("no use.\n");
    #endif

    return 0;
}

程序运行结果如下:
在这里插入图片描述

2.3.1 是否被定义

测试一个符号是否已被定义也是可行的.

在条件编译中完成这个任务更为方便,因为如果程序并不需要控制编译的符号所控制的特性,就不需要定义符号

#if defined(symbol)
#ifdef symbol

#if !defined(symbol)
#ifndef symbol

上面每对定义的两条语句是等价的,但是#if形式功能更强,可以添加其他需要判断的条件.


例如:

#include <stdio.h>

#define TEST1 1 //只定义TEST1

int main(void)
{
    #ifdef TEST1
        printf("TEST1 is defined.\n");
    #endif 

    #ifndef TEST1
        printf("TEST1 is undefined.\n");
    #endif

    #ifdef TEST2
        printf("TEST2 is defined.\n");
    #endif 

    #ifndef TEST2
        printf("TEST2 is undefined.\n");
    #endif

    return 0;
}

程序运行结果如下:
在这里插入图片描述

2.3.2 嵌套指令

if语句一样,条件编译也是可以嵌套的

#if defined(OS_UNIX)
    #ifdef OPTION1
        unix_version_option1();
    #endif
    #ifdef OPTION2
        unix_version_option2();
    #endif
#elif defined(OS_MSDOS)
    #ifdef OPTION2
        msdos_version_option2();
    #endif
#endif

为了方便记住复杂的嵌套指令,可以为每一个#endif加上一个注释标签,标签的内容就是#if#ifdef后面的表达式,例如:

#ifdef OPTION1
    ...
#else 
    ...
#endif  /* OPTION1 */

2.4 文件包含

#include指令是另一个文件的内容被编译,就像它实际出现于#include指令出现的位置一样.
这种替换执行的方式很简单: 预处理器删除这条指令, 并用包含文件的内容取而代之.
这样,如果一个头文件被包含到 10 个源文件中,实际上它被编译了 10 次

提示: 这意味着#include会涉及一些开销,但我们无需担心这种开销

    1. 如果两个源文件都需要同一组声明,我将这些声明放入一个头文件再声明和这些声明被分别包含所花费的时间相差无几
    1. 头文件包含是在预处理阶段执行的,并不会影响编译链接后程序实际的运行时间

提示:
当头文件被包含时,头文件中所有的内容都会被编译.这样最好将每组的函数或者数据的声明放在不同的头文件中.与将所有的声明放在一个巨大的头文件相比,前一种方法还是要更加好一点.

提示:
程序设计和模块化的原则也支持这种方法.只把必要的声明包含与一个头文件中要更加好一点,这样就不会访问到一些不想被访问的私有化的函数或者数据.同时也更好维护

2.4.1 函数库文件包含

编译器支持两种不同类型的#include文件包含: 函数库文件 和 本地文件.事实上,它们的区别很小.

函数库文件的包含使用以下语法:

#include <filename>

对于filename没有限制,但根据规定,标准库文件最好以.h后缀结尾(从技术上来说,函数库头文件并不需要以文件的形式存储,但是对于程序员来说,这并不会显而易见)

Linux系统中, 函数库头文件放置在/usr/include中:

在这里插入图片描述

2.4.2 本地文件包含

#include "filename"

查找策略: 现在源文件所在目录下查找,如果该头文件未找到,编译器就会查找函数库文件位置进行查找.
如果都查找不到就会提示编译错误

当然可以将所有的头文件都以""来包含
但是这样查找的效率就会低了,同时也分不清什么是函数库文件和本地文件了.

2.4.3 嵌套文件包含

嵌套的#include将使我们很难看清楚文件包含关系
例如:
在这里插入图片描述

comm.hcomm.c是公共模块
test1.htest1.c使用了公共模块
test2.htest2.c使用了公共模块
test.htest.c使用了test1test2模块
这样最终程序出现了两份comm.h的内容,造成了文件内容的重复.

如果comm.h文件内容很多,如果被重复了10次,源文件预处理后的代码量是不可以想象的!

使用条件编译就可以解决这个问题

#ifndef __TEST_H__
#define __TEST_H__
    //头文件的包含
#endif //__TEST_H__

或者现在直接使用#pragma once解决这个问题

#pragma once

2.5 其他指令

  • #error
#error text of error message

#error允许生成错误信息.

  • #line
#line number "string"

#line将修改__LINE__的值为number;如果添加了可选部分"string",则会修改__FILE__的值为string

  • #prgama

#pragma允许一些编译选项或者其他任何方式无法实现的一些处理方式
例如,有些编译器使用#pragma指令在编译过程中打开或者关闭清单显示,或者把汇编代码插入到C程序中.
预处理器将忽视它不认识的#pragma指令

本章完.

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/785126.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

计算机内存中的缓存Cache Memories

这篇写一下计算机系统中的缓存Cache应用场景和实现方式介绍。 Memory hierarchy 在讲缓存之前&#xff0c;首先要了解计算机中的内存结构层次Memory hierarchy。也就是下图金字塔形状的结构。 从上到下&#xff0c;内存层次结构如下&#xff1a; 寄存器&#xff1a;这是计算机…

自动化测试与手工测试比较

既然现在有了自动化测试&#xff0c;甚至现在许多团队在使用人工智能的方法&#xff0c;逐渐让机器来取代人的测试。那么作为测试工程师的人未来会不会消失?这是一个摆在许多人面前的一个严肃的问题。去年刚刚过世的著名的天体物理学家斯蒂芬威廉霍金(Stephen William Hawking…

Qt - macOS 安装配置

文章目录 一、关于 QT1.2 Qt的发展史1.3支持的平台1.4 Qt版本1.5 Qt 的优点1.6 成功案例 二、软件安装1、保证已 Xcode 和 Command Line Tools2、下载 QT3、下载 [qtcreator](http://download.qt.io/official_releases/qtcreator/)查看qt版本 三、创建工程Qt 常见用法 四、基础…

智能安全配电装置应用场景有哪些?

安科瑞 华楠 一、应用背景 电力作为一种清洁能源&#xff0c;给人们带来了舒适、便捷的电气化生活。与此同时&#xff0c;由于使用不当&#xff0c;维护不及时等原因引发的漏电触电和电气火灾事故&#xff0c;也给人们的生命和财产带来了巨大的威胁和损失。 为了防止低压配电…

音频转换工具怎么使用你了解吗?让我来跟你分享背后原理

相信小伙伴们平时都会听听喜欢的音乐放松心情吧&#xff0c;不过你是否遇到过&#xff0c;想在一个播放设备上放一首歌的时候&#xff0c;却发现不支持该格式&#xff1f;音频转换格式软件在我们的生活中扮演着越来越重要的角色。这些软件可以帮助我们将音频文件转换成不同的格…

炒股最好用的5个指标?_通达信公式

摘要: 炒股市场是一个充满挑战的领域&#xff0c;而找到可靠的指标来辅助投资决策是成功的关键之一。在众多的指标中&#xff0c;神奇指标网提供了五个被广泛认为是炒股最好用的指标。本文将详细介绍这五个指标&#xff0c;包括其原理和如何应用它们来辅助投资决策。 导言: …

蓝桥杯专题-真题版含答案-【图形排版】【包子凑数】【位反序数】【自守数】

点击跳转专栏>Unity3D特效百例点击跳转专栏>案例项目实战源码点击跳转专栏>游戏脚本-辅助自动化点击跳转专栏>Android控件全解手册点击跳转专栏>Scratch编程案例点击跳转>软考全系列点击跳转>蓝桥系列 &#x1f449;关于作者 专注于Android/Unity和各种游…

Java进阶 —— 多进程并发

前言 在系统学完Java的面向对象编程之后&#xff0c;我们需要认真地来学习Java并发编程&#xff0c;我们在学习计算机操作系统的时候也都了解过进程、线程和协程的概念。在这篇文章中荔枝主要会梳理有关线程创建、线程生命周期、同步锁和死锁、线程通信和线程池的知识&#xff…

android app控制ros机器人一

android开发app&#xff0c;进而通过控制ros机器人&#xff0c;记录开发过程 查阅资料&#xff1a; rosjava使用较多&#xff0c;已经开发好的app也有开源的案例 rosjava GitHub https://github.com/ros-autom/RobotCA https://github.com/ROS-Mobile/ROS-Mobile-Android…

因果推断(一)合成控制法(SCM)

因果推断&#xff08;一&#xff09;合成控制法&#xff08;SCM&#xff09; 在互联网时代&#xff0c;产品迭代速度越来越快&#xff0c;营销活动也越来越多。分析师因此需要快速的量化每次迭代或每次营销的效果&#xff0c;探索改变与结果之间的因果关系&#xff0c;并将优秀…

idea如何解决导入的项目不是Maven工程(文件下面没有蓝色的方格)二

简介&#xff1a; Maven项目导入&#xff0c;idea不识别项目 解决方法&#xff1a; 选中pom.xml -- 右键 -- Add as Maven Project

使用Python搭建代理服务器- 爬虫代理服务器详细指南

搭建一个Python爬虫代理服务器可以让你更方便地管理和使用代理IP。下面是一个详细的教程来帮助你搭建一个简单的Python爬虫代理服务器&#xff1a; 1. 首先&#xff0c;确保你已经安装了Python。你可以在官方网站(https://www.python.org/)下载并安装最新版本的Python。 2. 安…

Spring 中简单存取 Bean 的相关注解

目录 前言存储 Bean 对象五大类注解方法注解&#xff08;Bean&#xff09; 获取 Bean 对象 (Autowired)属性注入多个同类型 Bean 注入怎么办&#xff1f; Setter 注入构造方法注入&#xff08;官方推荐&#xff09; 前言 之前我们存储获取 Bean 的操作很繁琐&#xff0c;需要将…

在职硕士|2023级中国社科院-美国杜兰大学合办双证能源管理硕士(MME)

金融硕士 在职硕士|2023级中国社科院-美国杜兰大学合办双证能源管理硕士&#xff08;MME&#xff09; 中国社会科学院大学与美国杜兰大学合作举办的能源管理专业硕士学位教育项目&#xff08;UCASS-Tulane Master of Management in Energy&#xff0c;简称MME&#xff09;于2…

《人工智能安全》课程总体结构

1 课程内容 人工智能安全观&#xff1a;人工智能安全问题、安全属性、技术体系等基本问题进行了归纳整理。人工智能安全的主要数据处理方法&#xff0c;即非平衡数据分类、噪声数据处理和小样本学习。人工智能技术赋能网络空间安全攻击与防御&#xff1a;三个典型实例及攻击图…

mybatis_配置之属性优化

概念 别名优化&#xff1a; 类型别名可为 Java 类型设置一个缩写名字。 它仅用于 XML 配置&#xff0c;意在降低冗余的全限定类名书写。例如&#xff1a; 在xml文件里为SQL映射文件中定义返回值类型的属性起个别名 之后直接使用User进行使用 核心配置文件&#xff1a; MyBa…

常见面试题分享1

一、对JVM的了解 1.1 什么是JVM&#xff1f; JVM&#xff08;Java Virtual Machine&#xff09;&#xff0c;俗称Java虚拟机。它是一个虚构出来的计算机&#xff0c;是通过在实际的计算机上仿真模拟各种计算机功能来实现的。Java语言的一个非常重要的特点就是与平台的无关性。…

商城体系之产商品系统

本文主要讲解商城体系下产商品系统的设计。商城系统可以拆分成多个业务中台和多个应用服务。 1、产商品系统业务架构 产商品系统作为商城重要的基础信息组成部分&#xff0c;主要划分为产品信息和商品信息&#xff0c;产品信息保持最原始的产品基础属性和内容&#xff0c;商品…

下拉框可筛选可树状多选组件

实际效果图片 父页面 <el-form-item label"转发&#xff1a;" :label-width"formLabelWidth" class"formflex_item"><el-select ref"select" :clearable"true" clear"clearSelect" remove-tag"r…

day2 驱动开发 c语言

通过驱动开发给pcb板子点灯。 u-boot已经提前移植到了emmc中。 灯也是一种字符型设备。 编程流程需要先注册设备&#xff0c;然后创建结点&#xff0c;然后操作电灯相关寄存器 应用层直接调用read write来打开字符设备进行操作。 这样写会造成无法处理内核页面请求的虚拟地址…