1、基础数据

数据类型理解为固定内存大小的别名。

数据类型是创建变量的模子。

 2、有符号与无符号

数据类型的最高位用于标识数据的符号。

最高位为1,表明这个数为负数。

最高位为0,表面这个数为负号。

有符号数的表示法:

在计算机内部用补码表示有符号数。

正数的补码为正数本身。负数的补码为负数的绝对值各位取反后加一。

在计算机内部用源码表示无符号数。无符号数默认为正数,无符号数没有符号位。

对于固定长度的无符号数:

最大值加1变成了最小数,最小值减一得到最大值。

C语言中只有整数类型能够声明unsigned变量。

当一个有符号加一个无符号时,有符号数将被看做无符号数。

当无符号数与有符号数混合运算时,有符号将被看做无符号数。结果为无符号数。

无符号数当为0在减一就成为最大值。

3、浮点数的秘密

浮点数在内存的存储方式为:符号位,指数,尾数

浮点数的转换:

1、将浮点数转换成二进制。

2、用科学计数法表示二进制浮点数。

3、计算指数偏移后的值。

注意:计算指数时需要加上偏移量,而偏移量的值与类型有关。

示例:对于整数6,偏移后的值如下:

float:127+6--》133

double:1023+6--》1029

实数8.25在内存中的float表示:

8.25的二进制表示:1000.01-->1.0001*(2 ^ 3)

符号位:0

指数:127+3-->130-->10000010

小数:00001

内存中8.25的float表示:

010000010 000010000000000000000000000000000-->0x41040000

printf("0x%08x",*p) 用十六进制的形式打印p指的值。

浮点类型的秘密:

float能表示的具体数字的个数与int相同。

float可表示的数字之间不是连续的,存在间隙。

float只是一种近似的表示法,不能作为精确数使用。

由于内存表示法相对复杂,float的运算速度比int慢得多。

注意:double与float具有相同的内存表示法,因此double也是不精确的。由于double占用的内存较多。所能表示的精度比float高。

因为二进制?

四个字节能够用二进制完全表示3.1415吗?

表示的数字存在间隙。

小结:

浮点类型与整数类型的内存表示法不同。

浮点类型的内存表示更复杂。

浮点类型可表示的范围更大。

浮点类型是一种不精确的类型。

浮点类型的运算速度较慢。

4、类型转换

C语言中的数据类型可以进行转换

强制类型转换。隐式类型转换。

强制类型转换语法:

(Type)var_name;

(Type)value;

强制类型转换的结果:

目标类型能够容纳目标值:结果不变。

目标类型不能容纳目标值:结果将产生截断。

注意:不是所有的强制类型转换都能成功,当不能进行强制类型转换时,编译器将产生错误信息。

unsigned int p=(unsigned int)&ts;

在32位机器上,全局变量的地址占4个字节,32位,所以在32位的机器上上一句代码不会产生截断。

如果在64位机器上,全局变量的地址占8个字节,64位,强制转换为4个字节将产生截断。

基本类型不能转换为结构体,结构体也不能转换为基本类型。

隐式类型转换:

编译器主动进行的类型转换。

隐式类型转换的发生点:

算术运算中,低类型转换为高类型。char和int计算,char会转换为int。

赋值表达式中,表达式的值转换为左边变量的类型。

函数调用时,实参转换为形参的类型。

函数返回值,return表达式表达式转换为返回值类型。

小结:

标准c编译器的类型检查是比较宽松的,因此隐式类型转换可能带来意外的错误。

5、变量属性

C语言中变量可以有自己的属性。

在定义变量的时候可以加上属性关键字。

属性关键字指明变量的特有意义。

auto关键字:

auto即C语言中局部变量的默认属性。

auto表面将被修饰的变量存储于栈上。

编译器默认所有的局部变量都是auto的。

register关键字:

register关键字指明将局部变量存储于寄存器中。

register只是请求寄存器变量,但不一定请求成功。

register变量必须是cpu寄存器可以接受的值。

不能用&运算符获取register变量的地址。不是内存地址,寄存器哪有地址。

全局变量不应该为寄存器变量。第一个错误。

寄存器比内存快的多。

static关键字:

static关键字指明变量的静态属性:static修饰的布局变量存储在程序静态区。从栈到静态区,和全局变量的生命期相同。只是作用域在局部。

static关键字同时具有“作用域限定符”的意义:static修饰的全局变量作用域只是声明的文件中。

static修饰的函数作用域只是声明的文件中。

第一个打印:11111

第二个打印:12345  全局数据区分配空间

extern关键字:

extern用于声明“外部”定义的变量和函数:

extern变量在文件的其它地方分配空间。

extern函数在文件的其它地方定义。

extern用于“告诉”编译器用c方式编译:

全局变量定义在main函数之外,最后定义,没有报错? 但是之前要用extern int g_i; 声明一下。

在外边的文件定义时,另一个程序中有extern声明,需要同时编译这两个文件。变量加上static之后,extern就不管用了。报错了。

static和extern在某种意义上是互斥的。

6、分支语句

if语句中0值比较的注意点:

bool型变量应该直接出现于条件中,不要进行比较。

变量和0值比较时,0值应该出现在比较符号左边。能够检查出错误。立即数放左边。

float型变量不能直接进行0值比较,需要定义精度。不精度。

f==0;可能永远为不成立的。并不是真正的0zhi值。

switch语句对应单个条件多个分支的情形。

case语句分支必须要有break,否则会导致分支重叠。

default语句有必要加上,以处理特殊情况。

case语句中的值只能是整形或字符型。

case语句的排列顺序:

按字母或数字顺序排列各条语句。

正常情况放在前边,异常情况放在后面。

default语句只用于处理真正的默认情况。

7、循环语句

break和continue的区别:

break表示终止循环的执行。

continue表示终止本次循环,进入下次循环执行。

思考:switch能否用continue 关键字?为什么?

switch不是循环语句,而是分支语句,所有不能用continue。他只能用于循环语句。

8、goto和void分析

高手潜规则:禁用goto

void分析:

void修饰函数返回值和参数:

如果函数没有返回值,那么应该将其声明为void。

如果函数没有参数,应该声明其参数为void。

void 修饰函数返回值和参数是为了表示 “ 无 ” 。

编译能通过,c语言中,没有参数表示会接受任意多的参数。没有返回值默认为int。c语言不是一门强类型的语言。

所以不接受参数,或者返回任意类型,就需要使用void了。

void是一种类型,抽象的类型。抽象的存在。

不能定义void变量。没有定义他的大小是多少。

c语言没有定义void究竟是多大内存的别名。

没有void的标尺,无法在内存中裁剪出void对应的变量。

不能定义void类型的变量,但是可以定义void类型的指针。

因为void没有定义多大的内存别名,但是任意的指针是固定的要么4个字节的别名,要么8个字节的别名。

所以void* 可以定义。

小贴士:

ANSIC:标准c语言的规范。

扩展c:在ANSIC的基础上进行了扩充。

gcc扩展void大小为1.

bcc上是编译不过的。

void指针的意义:

c语言规定只有相同类型的指针才可以相互赋值。

void* 指针作为左值用于“接收”任意类型的指针。

void* 指针作为右值使用时需要进行强制类型转换。

9、const和volatile

const修饰的变量是只读的,本质还是变量。不能作为左值。

const修饰的局部变量在栈上分配空间。

const修饰的全局变量在全局数据区分配空间。(gcc在只读存储区?)

const只在编译器有用,在运行期无用。

const修饰的变量会在内存中分配空间,所以能够改变他修饰的变量的值。

const修饰的变量不是真的常量,他只是告诉编译器该变量不能出现在赋值符号的左边。

在现代C语言编译器中,修改const全局变量将导致程序奔溃。

注意:

标准C语言编译器不会将const修饰的全局变量存储于只读存储区,而是存储于可修改的全局数据区,其值依然可以改变。

const修饰的全局变量:

const修饰的全局变量存储在只读存储区的话,被修改值的时候会报错。gcc上是放到只读存储区:全局变量const。

bcc编译器编译器会把const全局数据放到全局数据区,通过指针可以改变他的值:因为bcc是一款比较早期的支持标准c语言规范的编译器,没有任何扩展和优化。

vc10.0:跟gcc一样,会优化。

const的本质:

C语言中的const使得变量具有只读属性。

现代c编译器中const将具有全局生命周期的变量存储于只读存储区。(全局变量和static变量)

const不能定义真正意义上的常量。

const修饰函数参数表示在函数体内不希望改变参数的值。

cosnt修饰函数返回值表示返回值不可改变,多用于返回指针的情形。

小贴士:

C语言中字符串字面量存储于只读存储区中,在程序中需要使用const char*指针。

深藏不露的volatile:

volatile可理解为“编译器警告指示字”。

volatile告诉编译器必须每次去内存中取变量值。去内存读值比较费时。

volatile主要修饰可能被多个线程访问的变量。

volatile也可以修饰可能被未知因素更改的变量。

如果多线程或中断中会改变obj的值,而编译器却优化了,违背了我们的意愿。此时需要volatile。

在多线程或者嵌入式中断的时候,需要考虑是不是要用volatile。

cont volatile int i=0;  //只读变量且不需要编译器优化。

出现i的时候去内存中取值,不能出现在赋值符号的左边。

10、struct和union分析

C语言中的struct可以看作变量的集合。

空结构体占用多大内存?

gcc中空结构体占用空间为0。

bcc,vc 编译器不允许空结构体的存在。

结构体与柔性数组:

柔性数组即数组大小待定的数组。

C语言中可以由结构体产生柔性数组。

C语言中结构体的最后一个元素可以是大小未知的数组。

大小为4,占用4个字节的空间。

C语言中的union:

C语言中的union在语法上与struct相似。

union只分配最大成员的空间,所有成员共享这个空间。

union的使用受系统大小端的影响:

c始终从低地址取数据。当是小端模式的时候,c的值为1,当是大端模式的时候,c的值为0.

判断系统的大小端:

小结:

struct中的每个数据成员有独立的存储空间。

struct可以通过最后的数组标识符产生柔性数组。

union中的所有数据成员共享同一个存储空间。

union的使用会受到系统大小端的影响。

11、enum、sizeof、typedef分析

enum是C语言中的一种自定义类型。

enum值是可以根据需要自定义的整形值。自定义的离散值。

第一个定义的enum值默认为0,。

默认情况下的enum值是在前一个定义值的基础上加1.

enum类型的变量只能取定义时的离散值。

green默认为0,blue默认值为3。

enum中定义的值是C语言中真正意义上的常量。

在工程中enum所用于定义整形常量。

打印:0x%08X:

sizeof关键字:

sizeof是编译器的内置指示符。

sizeof用于计算类型或变量所占的内存大小。

sizeof的值在编译器就已经确定。

sizeof用于类型:sizeof(type)

sizeof用于变量:sizeof(var)或 sizeof var

sizeof是C语言的内置关键字而不是函数。

在编译过程中所有的sizeof被具体的数值所替换。

程序的执行过程与sizeof没有任何关系。

打印  0,,,,4

sizeof(var++),var返回自身结果4,++没有执行?

sizeof不是函数,编译期间,就把所有sizeof的值计算出来并被数字代替了。

编译后就是直接 size=4了,没有计算这一步。

int f() {printf("sjfls\n");return 0; }

size=sizeof(f()); printf("size=%d\n",size);

函数里边的内容是不会打印出来的,因为编译期间就计算出来值了,代码不会执行到f().这个函数没有被调用。

typedef的意义:

面试中:

考官:你能说说typedef具体的意义吗?

应聘者:typedef用于定义一种新的类型。XXX

typedef用于给一个已经存在的数据类型重命名。

typedef本质上不能产生新的类型。

typedef重命名的类型:可以在typedef语句之后定义,不能被unsigned和signed修饰。新名字不能用有符号无符号修饰。

用法:typedef type new_name;

在typedef中定义数据无名struct并且重命名为SoftArray。

typedef struct _tag_list_node listNode;

告诉编译器遇到listNode就替换为struct _tag_list_node,而不管在哪里定义。

小结:

enum用于定义离散值类型。

enum定义的值是真正意义上的常量。

sizeof是编译器的内置指示符。编译时计算。

sizeof不参与程序的执行过程。

typedef用于给类型重命名:重命名的类型可以typedef语句之后定义。

12、注释符号

注释规则:编译器在编译过程中使用空格替换整个注释。

字符串字面量中的 // 和 /*...*/ 不代表注释符号。不是注释。

/*...*/型注释不能被嵌套。

\ 换行符

y=x/ *p;  就可以了, /*中间放一个空格就行了。

注释用于阐述原因和意图而不是描述程序的运行过程。

注释避免使用缩写。

注释避免臃肿和喧宾夺主。

13、接续符和转义符

C语言中的接续符(\)是指示编译器行为的利器。

编译器会将反斜杠剔除,跟在反斜杠后面的字符自动接续到前一行。

在接续单词时,反斜杠之后不能有空格,反斜杠的下一行之前也不能有空格。

接续符适合在定义宏代码块时使用。

宏代码块放到一行不太好看,用接续符清晰可读。

C语言中的转义符(\)主要用于表示无回显字符,也可用于表示常规字符。

当反斜杠(\)作为转义符使用时必须出现在单引号或者双引号之间。

char enter='\n';

char* p="\141\t\x62";   //运行结果为  a       b

\后边三个数字141表示某一字符的ascall码表示,转义\ 后的62表示以十六进制表示一个字符的ascall码。

小结:

C语言中的反斜杠(\)同时具有接续符和转义符的作用。

作为接续符使用时可直接出现在程序中。

作为转义符使用时需要出现在单引号或双引号之间。

14、单引号和双引号

C语言中的单引号用来表示字符字面量。

C语言中的双引号用来表示字符串字面量。

字符a和反斜杠。

"a" 字符串a表示内存地址,指针值。首地址。

printf("%s,%s,%s,p1,p1,p3");  段错误。访问低地址空间。

printf('\n'); 无回显字符,字符字面量 。对应ascall码 0x00000010。段错误 

printf("\n");  合法

小贴士:

字符字面量被编译为对应的ASCII码。 整数

字符串字面量被编译为对应的内存地址。 指针

printf的第一个参数被当成字符串内存地址。

内存的低地址空间不能在程序中随意访问。

 

地址低于这个值的空间0x08048000,是不能随意访问的。

示例:

while条件为假。

用字符串赋值给字符变量c。

混淆了字符和字符串概念

改:

char c=' ' ;  //保存空格的ascall码 。

让用户输入,当不是tab,回车,空格,程序停止。

小结:

单引号括起来的单个字符代表整数。ascall码

双引号括起来的字符代表字符指针。内存空间地址。

c编译器接受字符和字符串的比较,无任何意义。

c编译器允许字符串对字符变量赋值,只能得到错误。

字符串编译后,得到合法的内存地址。

15、逻辑运算符分析

逻辑运算符&&,||和!真的很简单吗?

下面的程序运行结束后,ijk的值分别为多少?

int i=0;

int j=0;

int k=0;

++i || ++j  && ++k;

与比或的优先级高

打印出1 0 0

先计算左边的,短路规则

程序中的短路:

|| 从左向右开始计算:

当遇到为真的条件时停止计算,整个表达式为真。

所有条件为假时表达式才为假。

程序中的短路:

||从左向右开始计算:

当遇到为真的条件时停止计算,整个表达式为真。

所有条件为假时表达式才为假。

&&从左向右开始计算:

当遇到为假的条件时停止计算,整个表达式为假。

所有条件为真时表达式才为真。

打印:In f()...

In main():1

!究竟是什么?

C语言中的逻辑非“!”只认得0,只知道见了0就返回1,因此当其碰见不是0时,其结果为0。

小结:

程序中的逻辑表达式遵从短路规则。

在&&与||混合运算时:

整个表达式被看做||表达式。

从左向右先计算&&表达式。

最后计算||表达式。

逻辑非!运算符只认得0

碰见0返回1,否则统统返回0.

只有0才代表假,其余的所有值均代表真。

16、位运算符分析:

C语言中的位运算符:

位运算符直接对bit位进行操作,其效率最高。

左移和右移注意点

左操作数必须为整数类型。不能是float类型。

char和short被隐式转换为int后进行移位操作。

右操作数的范围必须为:[0,31]

左移运算符<<将运算数的二进制位左移:

规则:高位丢弃,地位补0。

右移运算符>>把运算数的二进制位右移、

规则:高位补符号位,地位丢弃。

0x1<<2+3的值是什么?

oops :不确定的。

gcc认为左移-1就是右移一位。

放错准则:

避免位运算符,逻辑运算符和数学运算符同时出现在一个表达式中。

当位运算符,逻辑运算符和数学运算符需要同时参与运算时,尽量使用括号()来表达计算次序。

小技巧:

左移n位相当于乘以2的n次方,但效率比数学运算符高。

右移n位相当于除以2的n次方,但效率比数学运算符高。

交换两个数:

 

#include <stdio.h>
#define SWAP1(a, b)    \
{                      \
    int t = a;         \
    a = b;             \
    b = t;             \
}
#define SWAP2(a, b)    \
{                      \
    a = a + b;         \   //int不能放下会溢出。。。。
    b = a - b;         \
    a = a - b;         \
}
#define SWAP3(a, b)    \
{                      \
    a = a ^ b;         \   //异或
    b = a ^ b;         \   //a^b^b=a
    a = a ^ b;         \   //a^b^a=b
}
int main()
{
    int a = 1;
    int b = 2;
    printf("a = %d\n", a); 
    printf("b = %d\n", b);   
    SWAP3(a ,b); 
    printf("a = %d\n", a); 
    printf("b = %d\n", b);   
    return 0;
}

不借助t怎么实现?

123逼格越来越高。

 

位运算与逻辑运算不同:

位运算没有短路规则,每个操作数都参与运算。

位运算的结果为整数,而不是0或1.

位运算优先级高于逻辑运算优先级。

#include <stdio.h>

int main()
{
    int i = 0;
    int j = 0;
    int k = 0;
    if( ++i | ++j & ++k )
    {
        printf("Run here...\n");
    }   
    return 0;
}
注意:位运算而不是逻辑运算符。

小结:

位运算符只能用于整数类型。

左移和右移运算符的右操作数范围必须为[0,31]

位运算没有短路规则,所有操作数均会求值。

位运算的效率高于四则运算和逻辑运算。

运算优先级:四则运算>位运算>逻辑运算。

17、++和--操作符

++和--操作符对应两条汇编指令。

前置:变量自增(减)1,取变量值

后置:取变量值,变量自增1

int i=0;

(i++)+(i++)+(i++)=

(++i)+(++i)+(++i)=

vc先做三次加法,gcc先做两次自增,相加在自增。

原因:

C语言中只规定了++和--对应的相对执行次序。

++和--对应的汇编指令不一定连续运行。 这里说的取值跟自加一不一定连续。

在混合运算中,++和--的汇编指令可能被 打断执行。

++和--参与混合运算结果是不确定的。

笔试面试中的奇葩题:

1、++i+++i+++i   //++i++  开始处理=1++ 出错

2、a+++b==>a++ +b ,   a+ ++b  //a+++b ; 1+4=5 a==>2

编译器究竟如何解释?

贪心法:++,--表达式的阅读技巧

编译器处理的每个符号应该尽可能多的包含字符。

编译器以从左向右的顺序一个一个尽可能多的读入字符。

当读入的字符不可能和已读入的字符组成合法符号为止。

1出错,2得出a=2, 结果5

b=b/*p ;  // b/* 意识到后边是注释。都注释了。

int j=++i + ++i + ++i  //===》正确了

b=b/ *p  //正确了,/成了除号

空格的作用:

空格可以作为C语言中一个完整符号的休止符。

编译器读入空格后立即对之前读入的符号进行处理。 不贪心了?

小结:

++和--操作符在混合运算中的行为可能不同。

编译器通过贪心法处理表达式中的子表达式。

空格可以作为C语言中一个完整符号的休止符。

编译器读入空格后立即对之前读入的符号进行处理。

 

18、三目运算符和逗号表达式

(a?b:c)可以作为逻辑运算的载体。

本质是if else?

(a<b ? a: b)=3;  是错的

三目运算符返回的是值,不是变量。 11=3.

如果左值想要是变量,就需要用地址了。

* ( a<b ? &a :  &b) = 3  地址里边的内容改变为3。也就是a变量的值改为3.

三目运算符(a?b:c)的返回类型:

通过隐式类型转换规则返回b和c中的较高类型。

当b和c不能隐式转换到同一类型时将编译出错。

判断下面三目表达式的返回类型:

char c=0;

short s=0;

int i=0;

double d=0;

char * p="str";

printf("%d\n,sizeof(c? c  :  s)");  返回 int 4

printf("%d\n,sizeof(i? i  :  d)");   返回double 8

printf("%d\n,sizeof(d? d  :  p)");   error

逗号表达式:

逗号表达式是C语言中的"黏贴剂" 。

逗号表达式用于将多个子表达式连接为一个表达式。

逗号表达式的值为最后一个子表达式的值。

逗号表达式中的前N-1个子表达式可以没有返回值。

逗号表达式按照从左向右的顺序计算每个子表达式的值。

下面的程序输出什么?

int i=0;

while(i<5)

printf("i=%d\n",i),

i++

#include <stdio.h>

void hello()
{
    printf("Hello!\n");
}

int main()
{   
    int a[3][3] = {   ===>  int a[3][3] = { 2,5, 8}
        (0, 1, 2),
        (3, 4, 5),
        (6, 7, 8)
    };
    int i = 0;
    int j = 0;
    while( i < 5 )
        printf("i = %d\n", i),
    hello(),
    i++;
    for(i=0; i<3; i++)
    {
        for(j=0; j<3; j++)
        {
            printf("a[%d][%d] = %d\n", i, j, a[i][j]);  //打印了258,因为是逗号表达式
        }
    }

    return 0;
}

面试题:一行代码实现strlen:

#include <stdio.h>
#include <assert.h> //判断s是不是空指针,如果是打印错误信息

int strlen(const char* s)
{  
    return assert(s), (*s ? strlen(s + 1) + 1 : 0);
}

int main()
{   
    printf("len = %d\n", strlen("Delphi"));  // 6
    printf("len = %d\n", strlen(NULL)); //没有assert这句话会出错,段错误,加了之后出现失败的
    return 0;
}

小结:

三目运算符返回变量的值,而不是变量本身。

三目运算符通过隐式类型转换规则确认返回值类型。

逗号表达式按照从左向右的顺序计算每个子表达式的值。

逗号表达式的值为最后一个子表达式的值。

19、编译过程简介

编译器::预处理器 编译器 汇编器 链接器。

.o是二进制文件,

预编译:

处理所有的注释,以空格代替。

将所有的 #define删除,并且展开所有的宏定义。

处理条件编译指令 #if, #ifdef , #elif, #else , #endif

处理#include , 展开被包含的文件。

保留编译器需要使用的#pragma指令。

预处理器指令示例:

gcc -E file.c -o file.i

编译:

对预处理文件进行词法分析,语法分析和语义分析。

词法分析:分析关键字,标示符,立即数是否合法。

语法分析:分析表达式是否遵循语法规则。

语义分析:在语法分析的基础上进一步分析表达式是否合法。

分析结束后进行代码优化生成相应的汇编代码文件。

编译指令示例:gcc -S file.c -o file.s

汇编:

汇编器将汇编代码转变为机器的可以执行指令。

每条汇编语句几乎都对应一条机器指令。

汇编指令示例:gcc -c file.s -o file.o

小结:

编译过程分为预处理,编译,汇编和链接四个阶段。

预处理:处理注释,宏以及以#开头的符号。将头文件直接复制到源文件。

编译:进行词法分析,语法分析,和语义分析。

汇编:将汇编代码翻译为机器指令的目标文件。

20、链接过程简介

链接器的主要作用是把各个模块之间相互引用的部分处理好,使得各个模块之间能够正确的衔接。·

静态链接:
由链接器在链接时将库的内容直接加入到可执行程序中。

a.out与file1.o,file2.o,libc.a是相互独立的,可以独立执行。

Linux下静态库的创建和使用:

编译静态库源码:gcc -c lib.c -o lib.o  ==>

生成静态库文件:ar -q lib.a lib.o        ==>ar打包命令,将.a之后的所有目标文件都打入这个lib.a包里边,得到一个档案文件,这个档案文件叫做静态库

使用静态库编译:gcc main.c lib.a -o main.out

gcc -c slib.c -o slib.o

ar -q slib.a slib.o

gcc 20-1.c slib.a -o test.out

动态链接:

可执行程序在运行时才动态加载库进行链接。

库的内容不会进入可执行程序当中。

Linux下动态库的创建和使用:

编译动态库源码:gcc -shared dlib.c -o dlib.so

使用动态库编译:gcc main.c -ldl -o main.out  // ldl告诉编译器在编译时主程序会使用动态链接的方式

关键方式系统调用:

dlopen: 打开动态库文件。

dlsym: 查找动态库中的函数并返回调用地址。

dlclose: 关闭动态库文件。

20-2.c  :

#include <stdio.h>
#include <dlfcn.h>

int main()
{
    void* pdlib = dlopen("./dlib.so", RTLD_LAZY);

    char* (*pname)();
    int (*padd)(int, int);

    if( pdlib != NULL )
    {
        pname = dlsym(pdlib, "name");
        padd = dlsym(pdlib, "add");
 
        if( (pname != NULL) && (padd != NULL) )
        {
            printf("Name: %s\n", pname());
            printf("Result: %d\n", padd(2, 3));
        }

        dlclose(pdlib);
    }
    else
    {
        printf("Cannot open lib ...\n");
    }

    return 0;
}

dlib.c :

char* name()
{
    return "Dynamic Lib";
}
int add(int a, int b)
{
    return a + b;
}

 

gcc -shared dlib.c -o dlib.so   //编译生成动态库

gcc 20-2.c -ldl -o test.out     //编译主程序

 ./test.out  //运行

删了动态库就打不开动态库了, pdlib为空,打开不成功。出错了。

动态链接在链接的时候告诉编译器我们的源程序依赖于外部的一个动态库。

小结:

链接是指将目标文件,库文件链接为可执行程序。

根据链接方式的不同,链接过程可以分为:

静态链接:目标文件直接链接进入可执行程序。

动态链接:在程序启动后才动态加载目标文件。在程序启动后把动态库加载到内存中,动态的查找库里边的函数来进行调用。

为什么有两种方式?

应用程序的更新要更新程序的某一部分内容,这时动态链接的方式优势就非常明显了。替换某个动态库就能消除bug了。

软件升级的时候将最终程序链接到服务器上,将需要升级的动态库下载下来,然后程序重启之后就使用新的动态库了。

好处:部分更新应用程序。

静态链接好处:适用于小程序。就一个可执行文件,放到哪都能执行。因此根据用户的不同需求产生两种链接。

21、宏定义与使用分析

#define是预处理器处理的单元实体之一。

#define定义的宏可以出现在程序的任意位置。

#define定义之后的代码都可以使用这个宏。  

 

#define定义的宏常量可以直接使用

#define定义的宏常量本质为字面量。。字面量不占用内存。。。const常量占内存

下面的宏常量定义正确吗? 34 预处理通过,编译不通过

#define ERROR -1  对

#define PATH1 "D:\test\test.c"

#define PATH2 D:test\test.c

#define PATH3 D:\test\

test.c

test.1:

#define ERROR -1
#define PATH1 "D:\test\test.c"
#define PATH2 D:\test\test.c
#define PATH3 D:\test\
test.c

int main()
{
    int err = ERROR;
    char* p1 = PATH1;
    char* p2 = PATH2; 预处理通过 编译出错
    char* p3 = PATH3; 预处理通过,编译时出错
}

gcc -E test.c -o test.i  预处理

预处理器不会语法检查

所以预处理能通过,但是预处理之后代码不符合语法会报错。

宏定义表达式:

#define表达式的使用类似函数调用。

#define表达式可以比函数更强大。C语言中没办法定义一个函数求数组大小,但是宏可以

#define表达式比函数更容易出错。预处理器不会思考,直接替换

下面的宏表达式定义正确吗?

#define _SUM_(a,b) (a)+(b)

#define _MIN_(a,b) ((a) < (b)?(a):(b))

#define _DIM_(a) sizeof(a)/sizeof(*a)

21.2.c

// #include <stdio.h>

#define _SUM_(a, b) (a) + (b)
#define _MIN_(a, b) ((a) < (b) ? (a) : (b))
#define _DIM_(a) sizeof(a)/sizeof(*a)
int main()
{
    int a = 1;
    int b = 2;
    int c[4] = {0};

    int s1 = _SUM_(a, b);
    int s2 = _SUM_(a, b) * _SUM_(a, b);
    int m = _MIN_(a++, b);
    int d = _DIM_(c);

  printf("s1 = %d\n", s1);   //3
  printf("s2 = %d\n", s2);   //5==>文本替换之后 (a)+(b)*(a)+(b)=5
  printf("m = %d\n", m);    // 2==>文本替换之后((a++)<(b)?(a++):(b)); a++两次,返回b?
  printf("d = %d\n", d);      //4

  return 0;
}
 

宏表达式与函数的对比:

宏表达式被预处理器处理,编译器不知道宏表达式的存在。

宏表达式用“实参”完全替代形参,不进行任何运算。可能产生歧义。

宏表达式没有任何的“调用”开销。

宏表达式中不能出现递归定义。

#define _SUM_(n) ((n>0)?(_SUM_(N-1)+n):0)  //错误的,宏没有递归

int s=_SUM_(10);

有趣的问题:

宏定义的常量或表达式是否有作用域限制?

下面的程序合法吗?

// #include <stdio.h>

void def()
{
    #define PI 3.1415926
    #define AREA(r) r * r * PI
}

double area(double r)
{
    return AREA(r);
}

int main()
{
    double r = area(5);

    // printf("PI = %f\n", PI);
    // printf("d = 5; a = %f\n", r);
    
    return 0;
}
//代码是对的,宏没有作用域的概念,area函数是对的。因为宏是由预处理器处理的,编译器不知道宏标示符的存在。所以编译器无法将作用域的概念应用于这些标示符。

强大的内置宏:

_FILE_ ----字符串

综合示例:

#include <stdio.h>
#include <malloc.h>

#define MALLOC(type, x) (type*)malloc(sizeof(type)*x) //两个参数,申请x个type在堆空间

#define FREE(p) (free(p), p=NULL)  //释放p指针指向的堆空间,然后置为空,逗号表达式

#define LOG(s) printf("[%s] {%s:%d} %s \n", __DATE__, __FILE__, __LINE__, s)  //编译时间 ,文件名,行号,日志字符串

#define FOREACH(i, m) for(i=0; i<m; i++)  //逐个的遍历
#define BEGIN {
#define END   }

int main()
{
    int x = 0;
    int* p = MALLOC(int, 5);  //申请5个int,C语言中的函数没办法将类型作为参数来进行调用
    LOG("Begin to run main code...");
    FOREACH(x, 5)
    BEGIN
        p[x] = x;
    END
    
    FOREACH(x, 5)
    BEGIN
        printf("%d\n", p[x]);
    END
    
    FREE(p);
    
    LOG("End");
    
    return 0;
}

这些宏是经常用到的!!!

小结:

预处理器直接对宏进行文本替换。

宏使用时的参数不会进行求值和运算。

预处理器不会对宏定义进行语法检查。

宏定义时出现的语法错误只能被编译器检测。

宏定义的效率高于函数调用。

宏的使用会带来一定的副作用。

22、条件编译使用分析

宏定义和预处理器的应用

条件编译的行为类似于C语言中if else..

条件编译是预编译指示指令,用于控制是否编译某段代码。

条件编译的本质:

预编译器根据条件编译指令有选择的删除代码。

编译器不知道代码分支的存在。

而if...else..语句在运行期进行分支判断。

条件编译指令在预编译期进行分支判断。

可以通过命令行定义宏。

第二个编程试验:

命令行定义宏:gcc -DC=1 test.c

gcc -DC -E test.c -o test.i    //定义c

//#include <stdio.h>

int main()
{
    const char* s;

    #ifdef C
        s = "This is first printf...\n";
    #else
        s = "This is second printf...\n";
    #endif

    //printf("%s", s);
    
    return 0;
}

 

#include的本质:

#include的本质是将已经存在的文件内容嵌入到当前文件中。

#include的间接包含同样会产生嵌入文件内容的操作。

问题:间接包含同一个头文件是否会产生编译错误?

如果头文件中定义一个全局变量的话,会产生重复定义的错误。

解决方案:

#ifndef _GLOBAL_H_   定义这个宏,就不会重复包含了

#define _GLOBAL_H_

//source code

#endif

条件编译可以解决头文件重复包含的编译错误。

条件编译的意义:

条件编译使得我们可以按不同的条件编译不同的代码段,因而可以产生不同的目标代码。

#if...#else...#endif被预编译器处理,而if...else...语句被编译器处理,必然被编译进目标代码。

实际工程中条件编译主要用于以下两种情况:

不同的产品线共用一份代码。区分编译产品的调试版和发布版。

product.h:

#define DEBUG 1
#define HIGH  1

main.c:

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

#if DEBUG  //指示是调试版还是发布版
    #define LOG(s) printf("[%s:%d] %s\n", __FILE__, __LINE__, s)
#else
    #define LOG(s) NULL
#endif

#if HIGH  //是高端产品还是基础产品
void f()
{
    printf("This is the high level product!\n");
}
#else
void f()
{
}
#endif

int main()
{
    LOG("Enter main() ...");
    f();
    printf("1. Query Information.\n");
    printf("2. Record Information.\n");
    printf("3. Delete Information.\n");    
    #if HIGH
    printf("4. High Level Query.\n");
    printf("5. Mannul Service.\n");
    printf("6. Exit.\n");
    #else
    printf("4. Exit.\n");
    #endif
    LOG("Exit main() ...");
    return 0;
}

小结:

通过编译器命令行能够定义预处理器使用的宏。

条件编译可以避免重复包含同一个头文件。

条件编译是在工程开发中可以区别不同产品线的代码。

条件编译可以定义产品的发布版和调试板。

23、#error和#line使用分析

预处理器指示字。

#error用于生成一个编译错误消息。

用法:

#error message

message不需要用双引号包围。

#error编译指示字用于自定义程序员特有的编译错误消息。

类似的,#warning用于生成编译警告。

#error是一种预编译指示字。

#error可用于提示编译条件是否满足。

编译过程中的任意错误信息意味着无法生成最终的可执行程序。

#include <stdio.h>

#ifndef __cplusplus //如果用错了编译器,会报下边的错误。
    #error This file should be processed with C++ compiler.
#endif

class CppClass
{
private:
    int m_value;
public:
    CppClass()
    {        
    }    
    ~CppClass()
    {
    }
};

int main()
{
    return 0;
}
23-2.c : #error用法

#include <stdio.h>

void f()
{
#if ( PRODUCT == 1 )
    printf("This is a low level product!\n");
#elif ( PRODUCT == 2 )
    printf("This is a middle level product!\n");
#elif ( PRODUCT == 3 )
    printf("This is a high level product!\n");

#else

    #error the macro prodect is not defined! //加的
#endif
}

int main()
{
    f();
    printf("1. Query Information.\n");
    printf("2. Record Information.\n");
    printf("3. Delete Information.\n");

#if ( PRODUCT == 1 )
    printf("4. Exit.\n");
#elif ( PRODUCT == 2 )
    printf("4. High Level Query.\n");
    printf("5. Exit.\n");
#elif ( PRODUCT == 3 )
    printf("4. High Level Query.\n");
    printf("5. Mannul Service.\n");
    printf("6. Exit.\n");

#else

    #warning the macro prodect is not defined! //加的

#endif
    return 0;
}

gcc -DPRODUCT=2 test.c  //中端产品

gcc -DPRODUCT=1 test.c  //低端产品

gcc -DPRODUCT=3 test.c  //高端产品

如果在命令行忘了定义宏:怎么办?

可以加#error.  #warning

#line的用法:

#line用于强制指定新的的行号和编译文件名,并对源程序的代码重新编号。

用法:

#line number filename

filename 可省略

#line编译指示字的本质是重定义__LINE__和__FILE__

 #include <stdio.h>

// The code section is written by A.
// Begin
#line 1 "a.c"

// End

// The code section is written by B.
// Begin
#line 1 "b.c"

// End

// The code section is written by Delphi.
// Begin
#line 1 "delphi_tang.c"
int main()
{
    printf("%s : %d\n", __FILE__, __LINE__);
    printf("%s : %d\n", __FILE__, __LINE__);
    return 0;
}

// End

小结:

#error用于自定义一条编译错误信息。

#warning用于自定义一条编译警告信息。

#error和#warning常应用于条件编译的情形。

#line用于强制指定新的的行号和编译文件名。

24、#pragma使用分析

预处理器指示字。

#pragma用于指示编译器完成一些特定的动作。

#pragma所定义的很多指示字是编译器特有的。

#pragma在不同的编译器间是不可移植的。

预处理器将忽略它不认识的#pragma指令。

不同的编译器可能以不同的方式解释同一条#pragma指令。

一般用法:

#pragma parameter

注:不同的parameter参数语法和意义各不相同。

#pragma message:

message参数在大多数的编译器中都有相似的实现。

message参数在编译时输出消息到编译输出窗口中。

message用于条件编译中可提示代码的版本信息。

#if defined(ANDROID20)

   #pragma message("Comile Android SDK 2.0...")

  #define VERSION "Android 2.0"

#endif

与#error和#warning不同,#pragma message仅仅代表一条编译信息,不代表程序错误。

#include <stdio.h>

#if defined(ANDROID20)
    #pragma message("Compile Android SDK 2.0...")
    #define VERSION "Android 2.0"
#elif defined(ANDROID23)
    #pragma message("Compile Android SDK 2.3...")
    #define VERSION "Android 2.3"
#elif defined(ANDROID40)
    #pragma message("Compile Android SDK 4.0...")
    #define VERSION "Android 4.0"
#else
    #error Compile Version is not provided!
#endif

int main()
{
    printf("%s\n", VERSION);

    return 0;
}
 

gcc -DANDROID40 test.c

#pragma once

#pragma once用于保证头文件只被编译一次。

#pragma once是编译器相关的,不一定被支持。

左边被C语言支持,包含多次,但是嵌入一次,预处理器处理了多次。

右边直接包含一次,效率高。有些编译器不支持。

global.h:

#pragma once

int g_value = 1;

main.c:

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

int main()
{
    printf("g_value = %d\n", g_value);

    return 0;
}
bcc编译器不支持#pragma once

解决方案:

#ifndef _GLOBAL_H_

#define _GLOBAL_H_

#pragma once

int g_value=1;

#endif

既保证效率,又保证了移植性。

#pragma pack:

什么是内存对齐?

不同类型的数据在内存中按照一定的规则排列。

而不一定是顺序的一个接一个的排列。

为什么需要内存对齐?

CPU对内存的读取不是连续的,而是分成块读取的,块的大小只能是1、2、4、8、16...字节

当读取操作的数据未对齐,则需要两次总线周期来访问内存,因此性能会大打折扣。

某些硬件平台只能从规定的相对地址处读取特定类型的数据,否则产生硬件异常。

#pragma pack用于指定内存对齐方式。

#pragma pack能够改变编译器的默认对齐方式:

#pragma pack(1)  打印都是8.

#include <stdio.h>

#pragma pack(1)
struct Test1
{                          //对齐参数    偏移地址     大小
    char  c1;        //1                    0                  1
    short s;          //2                    1                  2
    char  c2;       //1                     3                  1
    int   i;             //4                     4                  4
};
#pragma pack()

#pragma pack(1)
struct Test2
{                          //对齐参数    偏移地址     大小
    char  c1;        //1                   0                   1
    char  c2;       //1                    1                   1
    short s;         //2                     2                  2
    int   i;            //4                     4                  4
};
#pragma pack()

int main()
{
    printf("sizeof(Test1) = %d\n", sizeof(struct Test1));
    printf("sizeof(Test2) = %d\n", sizeof(struct Test2));

    return 0;
}

 

struct占用的内存大小:

第一个成员起始于0偏移处。

每个成员按其类型大小和pack参数中较小的一个进行对齐:

偏移地址必须能被对齐参数整除。

结构体成员的大小取其内部长度最大的数据成员作为其大小。(包含结构体,如下边的包含S1)

结构体总长度必须为所有对齐参数的整数倍:

编译器在默认情况下按照4字节对齐.

#include <stdio.h>

#pragma pack(4)
struct Test1
{                          //对齐参数    偏移地址     大小
    char  c1;        //1                    0                  1
    short s;          //2                    2                   2
    char  c2;       //1                     4                   1
    int   i;             //4                     8                   4
};
#pragma pack()

#pragma pack(4)
struct Test2
{                          //对齐参数    偏移地址     大小
    char  c1;        //1                   0                   1
    char  c2;       //1                    1                   1
    short s;         //2                     2                  2
    int   i;            //4                     4                  4
};
#pragma pack()

int main()
{
    printf("sizeof(Test1) = %d\n", sizeof(struct Test1));
    printf("sizeof(Test2) = %d\n", sizeof(struct Test2));

    return 0;
}


24-3.c :

#include <stdio.h>

#pragma pack(8)

struct S1
{                          //对齐参数    偏移地址     大小
    short a;          //2                    0                    2
    long b;           //4                    4                    4  
};

struct S2            //24
{                          //对齐参数    偏移地址     大小
    char c;            //1                      0               1
    struct S1 d;    //4                      4               8
    double e;       //8                      16             8    //(gcc对齐参数变成4) 偏移地址为12,
};

#pragma pack()

int main()
{
    printf("%d\n", sizeof(struct S1));
    printf("%d\n", sizeof(struct S2));

    return 0;
}

gcc::8  20  gcc不支持8字节对齐  删掉#pragma pack(8),四字节对齐

vc :: 8  24

小结:

#pragma用于指示编译器完成一些特定的动作。

#pragma所定义的很多指示字是编译器特有的。

-- #pragma message用于自定义编译消息。

-- #pragma once 用于保证头文件只被编译一次。

-- #pragma pack 用于指定内存对齐方式。

25、#和##操作符使用分析

#运算符用于在预处理期将宏参数转换为字符串。

#的转换作用是在预处理期完成的,因此只在宏定义中有效。

编译器不知道#的转换作用。

用法:

#define STRING(x) #x

printf("%s\n",STRING(Hello World!));

25-1.c:

#include <stdio.h>

#define STRING(x) #x

int main()
{
    printf("%s\n", STRING(Hello world!));  //打印字符串
    printf("%s\n", STRING(100));
    printf("%s\n", STRING(while));
    printf("%s\n", STRING(return));

    return 0;
}
 

25-2.c:

#include <stdio.h>  //调用函数的时候,怎么动态的打印函数名?函数名是编程的时候起的名字,不是C语言里面合法的字符串,只能用#运算符

#define CALL(f, p)  (printf("Call function %s\n", #f), f(p))  //#f 将函数名转换为字符串 #f “square"
int square(int n)
{
    return n * n;
}

int func(int x)
{
    return x;
}

int main()
{
    int result = 0;
    result = CALL(square, 4);
    printf("result = %d\n", result);
    result = CALL(func, 10);
    printf("result = %d\n", result);

    return 0;
}

##运算符:

##运算符用于在预处理期粘贴两个标识符。

##的连接作用是在预处理期完成的,因此只在宏定义中有效。

编译器不知道##的连接作用

用法:

#include <stdio.h>

#define NAME(n) name##n //连接在一起

int main()
{
    
    int NAME(1);  //name1
    int NAME(2);  //name2
    
    NAME(1) = 1;  //name1=1;
    NAME(2) = 2;  //name2=2;
    
    printf("%d\n", NAME(1));  //1
    printf("%d\n", NAME(2));  //2

    return 0;
}
有什么用?

struct Student

{ int id;

 char* name;

};

struct Student s1;

s1.id=1;

s1.name="delhbl";

如果定义的结构体比较多的话,每次都写struct是不是累?

定义 typedef struct  _tag_student Student;

定义的时候:Student s1;

不同敲struct关键字了。但是定义成千上百的结构体的时候,要重复的写上typedef struct 这句话

如何提高效率?所以提出##运算符。 定义STRUCT宏

#include <stdio.h>

#define STRUCT(type) typedef struct _tag_##type type;\
                     struct _tag_##type

//结果:

//typedef struct _tag_Student Student;

//struct _tag_Student

//{ char* name;

// int id;

//}

STRUCT(Student)  //括号()中的结构体类型是我们随意定义的结构体,也可以定义STRUCT(Shuai) ,Shuai这个结构体
{
    char* name;
    int id;
};

int main()
{
    
    Student s1;
    Student s2;
    s1.name = "s1";
    s1.id = 0;
    s2.name = "s2";
    s2.id = 1;
    printf("s1.name = %s\n", s1.name);
    printf("s1.id = %d\n", s1.id);
    printf("s2.name = %s\n", s2.name);
    printf("s2.id = %d\n", s2.id);

    return 0;
}


小结:

#运算符用于在预处理期将宏参数转换为字符串。

##运算符用于在预处理期粘贴两个标识符。

编译器不知道#和##运算符的存在。

#和##运算符只在宏定义中有效。

29、指针和数组分析下

以下标的形式访问数组中的元素。

以指针的形式访问数组中的元素。

指针以固定增量在数组中移动时,效率高于下标形式。
指针增量为1且硬件具有增量模型时,效率更高。

下标形式与指针形式的转换:

a[n]<=>*(a+n)<=>*(n+a)<=>n[a]

注意:现代编译器的生成代码优化率已大大提高,在固定增量时,下标形式的效率已经和指针形式相当,但从可读性的代码维护的角度来看,下标形式更优。

 

注意:现代编译器的生成代码优化率已大大提高,在固定增量时,下标形式的效率已经和指针形式相当,但从可读性的代码维护的角度来看,下标形式更优。

#include <stdio.h>

int main()

{

    int a[5] = {0};

    int* p = a;

    int i = 0;

    for(i=0; i<5; i++)

    {

        p[i] = i + 1;

    }

    for(i=0; i<5; i++)

    {

        printf("a[%d] = %d\n", i, *(a + i));

    }

    printf("\n");

    for(i=0; i<5; i++)

    {

        i[a] = i + 10; //==>a[i]=i+10

    }

    for(i=0; i<5; i++)

    {

        printf("p[%d] = %d\n", i, p[i]);

    }

    return 0;

}

指针也能当作数组来使用。

#include <stdio.h>

int main()

{

    extern int* a;

    printf("&a = %p\n", &a); 打印出地址 // 这个文件中的&a才是另一个文件中的a?

    printf("a = %p\n", a);    到上边的地址中取四个字节。0x1

    printf("*a = %d\n", *a);  *(0x1), 出错了。

    return 0;

}

在ext中a是一个数组。

int a[] = {1, 2, 3, 4, 5};

数组名可以看做指针但是实际上跟指针不一样。

运行:段错误。

a和 &a的区别:

a为数组首元素的地址。

&a为整个数组的地址。

a和 &a 的区别在于指针运算。

 

递增递减的步长不一样,第一个加了一个元素的长度,第二个加了整个数组的长度。

实例分析3:

#include <stdio.h>

int main()

{

    int a[5] = {1, 2, 3, 4, 5};

    int* p1 = (int*)(&a + 1);   //指向5后边的位置,p1[-1]==>*(p1-1)

int* p2 = (int*)((int)a + 1); //p2=0x804a014+1=0x804a015, p2[0]=>*(p2+0)=>*p2,

//指向第二个字节,取得值是0002,则linux(小端系统)下为*p2=0x02000000->335544325

    int* p3 = (int*)(a + 1);   //第二个元素

    printf("%d, %d, %d\n", p1[-1], p2[0], p3[1]);

    return 0;

}

三种答案:

// A. 数组下标不能是负数,程序无法运行

// B. p1[-1]将输出随机数,p2[0]输出2, p3[1]输出3

// C. p1[-1]将输出乱码, p2[0]和p3[1]输出2

结果:5, 335544325, 3

数组参数:

数组作为函数参数时,编译器将其编译成对应的指针。

 

结论:

一般情况下,当定义的函数中有数组参数时,需要定义另一个参数来标示数组的大小。

#include <stdio.h>

void func1(char a[5])

{

    printf("In func1: sizeof(a) = %d\n", sizeof(a));

    *a = 'a';

    a = NULL; //退化为指针

}

void func2(char b[])

{

    printf("In func2: sizeof(b) = %d\n", sizeof(b));

    *b = 'b';

    b = NULL;

}

int main()

{

    char array[10] = {0};

    func1(array);

    printf("array[0] = %c\n", array[0]);

    func2(array);

    printf("array[0] = %c\n", array[0]);

    return 0;

}

//打印4,a,4,b

小结:

数组名和指针仅使用方式相同。

--数组名的本质不是指针。

--指针的本质不是数组。

数组名并不是数组的地址,而是数组首元素的地址。

函数的数组参数退化为指针。

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值