C语言的30个夜晚

C语言标准库

C语言是一种通用的、面向过程式的计算机程序设计语言。1972 年,为了移植与开发 UNIX 操作系统,丹尼斯·里奇在贝尔电话实验室设计开发了 C 语言。
C标准库是一组 C 内置函数、常量和头文件,比如 stdio.hstdlib.hmath.h 等等。这个标准库可以作为 C 程序员的参考手册。
C语言标准库有各种不同的实现,比如glibc, 用于嵌入式Linux的uClibc,还有ARM公司的C语言标准库及精简版的MicroLib等。不同标准库的实现并不相同,而且提供的函数也不完全相同,不过有一个它们都支持的最小子集,这也就是最典型的C语言标准库。
典型的C语言标准库(GNU C 标准库)官网:The GNU C Library

glibc和libc有什么区别呢?
libc是Linux下的ANSI C的函数库;
glibc是Linux下的GUN C函数库;
ANSI C和GNU C有什么区别呢?
ANSI C是基本的C语言函数库,包含了C语言最基本的库函数。这个库可以根据 头文件划分为 15 个部分,其中包括:字符类型 (<ctype.h>)、错误码 (<errno.h>)、 浮点常数 (<float.h>)、数学常数 (<math.h>)、标准定义 (<stddef.h>)、 标准 I/O (<stdio.h>)、工具函数 (<stdlib.h>)、字符串操作 (<string.h>)、 时间和日期 (<time.h>)、可变参数表 (<stdarg.h>)、信号 (<signal.h>)、 非局部跳转 (<setjmp.h>)、本地信息 (<local.h>)、程序断言 (<assert.h>) 等等。这在其他的C语言的IDE中都是有的。
而GNU C函数库是一种类似于第三方插件的东西,由于Linux是用C语言写的,所以Linux的一些操作是用C语言实现的,所以GNU组织开发了一个C语言的库 用于我们更好的利用C语言开发基于Linux操作系统的程序。其实我们可以把它理解为类似于Qt是一个C++的第三方函数库一样。

标准库 内容
<stdio.h> 输入和输出
<stdlib.h> 最常用的一些系统函数
<string.h> 字符串处理
<math.h> 定义了一系列数学函数
<ctype.h> 包含了一系列字符类测试函数
<time.h> 时间和日期
<stdarg.h> 可变参数列表
<signal.h> 信号
<assert.h> 声明断言
<setjmp.h> 非局部跳转
<errno.h> 定义错误代码和处理错误的函数
<stddef.h> 一些常数、类型和变量
<locale.h> 定义了特定地域的设置,比如日期格式和货币符号
<float.h> 包含了一组与浮点值相关的常量和运算
<limits.h> 定义整数数据类型的取值范围

C语言错误处理和调试

C 语言不提供对错误处理的直接支持,但是作为一种系统编程语言,它以返回值的形式提供底层数据。在发生错误时,大多数的 C 或 UNIX 函数调用返回 1 或 NULL,同时会设置一个错误代码 errno,该错误代码是全局变量,表示在函数调用期间发生了错误。可以在 errno.h 头文件中找到各种各样的错误代码。
所以,C 程序员可以通过检查返回值,然后根据返回值决定采取哪种适当的动作。开发人员应该在程序初始化时,把 errno 设置为 0,这是一种良好的编程习惯。0 值表示程序中没有错误。

C语言错误处理一例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>
#include <errno.h>
#include <string.h>

extern int errno ;

int main (){
FILE * pf;
int errnum;
pf = fopen ("unexist.txt", "rb");
if (pf == NULL){
errnum = errno;
fprintf(stderr, "错误号: %d\n", errno);
perror("通过 perror 输出错误");
fprintf(stderr, "打开文件错误: %s\n", strerror( errnum ));
}else{
fclose (pf);
}
return 0;
}

C语言调试

Linux下的C语言程序调试,可以使用GDB。这篇文章记录了GDB调试器基本使用,更多的调试技术,后续再研究。

C语言输入输出

getchar() & putchar()

一次只能输入输出一个字符。
Example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>

int main(){
int c;

printf( "Enter a value :");
c = getchar( );
printf( "\nYou entered: ");
putchar( c );
printf( "\n");

char *s = "abc";
int i =0;
for ( i=0;i<sizeof(s);i++){
putchar(s[i]);
putchar('\n');
}

return 0;
}

gets() & puts()
char gets(char s) 函数从 stdin 读取一行到 s 所指向的缓冲区,直到一个终止符或 EOF。
int puts(const char *s) 函数把字符串 s 和一个尾随的换行符写入到 stdout。

Example:

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>

int main(){
char str[100];
printf( "Enter a value :");
gets( str );
printf( "\nYou entered: ");
puts( str );
return 0;
}

scanf() 和 printf()
int scanf(const char *format, …) 函数从标准输入流 stdin 读取输入,并根据提供的 format 来浏览输入。

int printf(const char *format, …) 函数把输出写入到标准输出流 stdout ,并根据提供的格式产生输出。

format 可以是一个简单的常量字符串,但是您可以分别指定 %s、%d、%c、%f 等来输出或读取字符串、整数、字符或浮点数。还有许多其他可用的格式选项,可以根据需要使用。

Example:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
int main() {

char str[100];
int i;

printf( "Enter a value :");
scanf("%s %d", str, &i);

printf( "\nYou entered: %s %d ", str, i);
printf("\n");
return 0;
}

余生疯狂做产品!跟这个世界继续死磕!

今晚复习跟数据结构息息相关的几个知识点。

存储类型

C语言中有以下类型的存储方式:

  • auto
    • auto用于声明变量的生存期为自动,即将不在任何类、结构、枚举、联合和函数中定义的变量视为全局变量,而在函数中定义的变量视为局部变量。
    • 因为auto是C语言变量默认的存储类型,所以一般不写。
  • register
    • register要求编译器尽可能的将register类型的变量存在CPU内部寄存器中而不是通过内存寻址的RAM中,以便提高效率。
  • static
    • static指示编译器在程序的生命周期内保持局部变量的存在,而不需要在每次它进入和离开作用域时进行创建和销毁。使用 static修饰局部变量可以在函数调用之间保持局部变量的值。
    • static修饰符也可以应用于全局变量,当 static修饰全局变量时,会使变量的作用域限制在声明它的文件内。
    • static是全局变量的默认存储类。
  • extern
    • extern存储类用于提供一个全局变量的引用,全局变量对所有的程序文件都是可见的。当使用extern时,对于无法初始化的变量,会把变量名指向一个之前定义过的存储位置。
    • 有多个文件且定义了一个可以在其他文件中使用的全局变量或函数时,可以在其他文件中使用extern来得到已定义的变量或函数的引用。extern是用来在另一个文件中声明一个全局变量或函数。
  • const
    • const要求其所修饰的对象为常量,不可对其修改和二次赋值操作(不能作为左值出现)。
  • volatile 修饰符
    • volatile提醒编译器它后面所定义的变量随时都有可能改变,因此编译后的程序每次需要存储或读取这个变量的时候,告诉编译器对该变量不做优化,都会直接从变量内存地址中读取数据,从而可以提供对特殊地址的稳定访问。

C语言const与static的区别

  • const 就是只读,只在声明中使用;
  • static 一般有两个作用,规定作用域和存储方式。

对于局部变量, static规定其为静态存储方式, 每次调用的初始值为上一次调用的值,调用结束后存储空间不释放;
对于全局变量, 如果以文件划分作用域的话,此变量只在当前文件可见; 对于static函数也是在当前模块内函数可见;
static const 是上面两者的合集。

结构体

结构体的声明方式:

1
2
struct 结构体名
{成员列表};

结构体声明样例:

1
2
3
4
5
6
7
8
9
struct student
{
int num;
char name[20];
char sex;
int age;
float score;
char addr[30];
};

声明的结构体相当于一个模型,但其中并无具体数据,系统对之也不分配实际内存单元,为了能在程序中使用结构类型的数据,应当定义结构体类型的变量,并在其中存放具体的数据。

结构体的嵌套

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct date{
int month;
int day;
int year;
}

struct student{
int num;
char name[20];
char sex;
int age;
struct date birthday;
char addr[30];
}student1, student2;

结构体变量的赋值与引用

例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
#include <string.h>

struct student{
int num;
char name[20];
char sex;
int age;
float score;
char addr[30];
};

int main (){
struct student s1;
s1.num = 1;
strcpy(s1.name,"xiaoming");//不能直接用s1.name="xiaoming"这种方式。
printf("%d: %s", s1.num,s1.name);
}

数组初始化赋值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>

struct student{
int num;
char name[20];
char sex;
int age;
float score;
char addr[30];
};

int main (){
struct student s1 = {1,"xiaoming",'F',10,99.9,"Beijing"};
// 单引号用于字符,双引号用于字符串(字符数组)。
printf("%d: %s", s1.num,s1.name);
}

.->的区别
一般情况下用.只需要声明一个结构体,格式是结构体类型名+结构体名。然后用结构体名加.加域名就可以引用域 了。因为自动分配了结构体的内存。如同int a一样。
而用->则要声明一个结构体的指针,还要手动开辟一个该结构体的内存,然后把返回的指针给声明的结构体指针,才能用->正确引用。

结构体数组

1
2
3
4
5
6
7
8
9
10
struct student{
int mum;
char name[20];
char sex;
int age;
float score;
char addr[30];
} stu[3] = {{10101,"Tom", 'M', 18, 87.5, "Beijing Road"},
{10102,"Bob", 'M', 18, 87.5, "Beijing Road"},
{10103,"Jim", 'M', 18, 87.5, "Beijing Road"} };

结构体变量指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <string.h>
#include <stdio.h>
#include <stdlib.h>

struct student {
long num;
char name[20];
char sex;
float score;
};

void main(){
struct student stu_1;
struct student *p;
p = &stu_1;
stu_1.num = 89101;
strcpy(stu_1.name, "Li Lin");
stu_1.sex = 'M';
stu_1.score = 89.5;
printf("NO. :%ld\nname: %s\nsex:%c\nscore:%f\n", stu_1.num, stu_1.name, stu_1.sex, stu_1.score);
printf("NO. :%ld\nname: %s\nsex:%c\nscore:%f\n", (*p).num, (*p).name, (*p).sex, (*p).score);
printf("NO. :%ld\nname: %s\nsex:%c\nscore:%f\n",p->num, p->name, p->sex, p->score);
system("pause");
}

结构体数组指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <stdio.h>
#include <stdlib.h>

#define FORMAT "%d\t%-3s\t%1c\t%2d\n"

struct student{
int num;
char name[20];
char sex;
int age;
};

struct student stu[3] = {
{1, "Tom", 'M', 18},
{2, "Kim", 'M', 19},
{3, "Jim", 'F', 20}
};

int main(){
struct student *p;
printf("No.\tname\tsex\tage\n");
for(p=stu; p<stu+3;p++)
printf(FORMAT, p->num, p->name, p->sex, p->age);
}

链表

链表是一种常见的数据结构,用于动态地进行存储分配。
以单向链表为例,它有一个头指针变量,存放一个地址,该地址指向一个元素。链表中每一个元素称为结点,每个结点都应包括两个部分,一为用户需要用的实际数据,二为下一个结点的地址。可以看出,头指针head指向第一个元素,第一个元素又指向第二个元素,直到最后一个元素,最后一个元素不再指向其他元素,称之为表尾,它的地址部分放一个NULL(表示空地址),链表到此结束。

单向链表示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <stdio.h>
#include <stdlib.h>

struct student{
long num;
float score;
struct student *next;
};

void main(){
struct student s1, s2, s3, *head, *p;
s1.num = 1; s1.score = 89.5;
s2.num = 2; s2.score = 90;
s3.num = 3; s3.score = 85;
head = &s1; //将结点 a 的起始地址赋给头指针 head
s1.next = &s2; //将结点 b 的起始地址赋给 a 结点的 next 成员
s2.next = &s3;
s3.next = NULL; // c 结点的 next 成员不存放其他结点地址
p = head;//使 p 指针指向 a 结点
do{
printf("%ld %5.1f\n", p->num, p->score);// 输出 p 指向的结点的数据
p = p->next; //使 p 指向下一结点
}while(p != NULL);//输出完 c 结点后 p 的值为 NULL
}

链表类型

链表类型有单向链表双向链表循环链表等类型。

双向链表是指在单向链表的基础上,每个链表元素既有指向下一个元素的指针,又有指向前一个元素的指针,其中每个结点都有两种指针,即front和tail,front指针指向左边结点,tail指针指向右边结点。

循环链表指的是在单向链表和双向链表的基础上,将两种链表的最后一个结点指向第一个结点从而实现循环。

Windows堆表中的“快表(快速单向链表)”就是使用了单向链表作为其数据结构,“空表(空闲双向链表)”则是一种双向链表。

参考资料

数据结构之队列、栈和链表(一)
数据结构之队列、栈和链表(二)

今晚复习C语言的指针和数组。

指针

指针:指向一个变量的地址。

声明方式:

1
2
int *p;
char *q;
C语言**ptr**变量名的含义:pts就是**Pointer Recod(er)**的简写

指针的使用:定义指针变量、把变量地址赋值给指针、访问指针变量指向的地址的值。

空指针:指向0x0,啥也不是。

指针的值:指针的值的类型都是一样的,长度等于一个单位地址的长度的16进制数值(32位、64位等)

指针的类型:指针指向的数据类型必须是一个有效的 C 数据类型。

例如,在32位系统中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>

int main (){
int *p;
char *t;
printf("Length of p: %d\n", sizeof(p));
printf("Length of t: %d\n", sizeof(t));
printf("Length of *p: %d\n", sizeof(*p));
printf("Length of *t: %d\n", sizeof(*t));
}

/*
执行结果:
Length of p: 4
Length of t: 4
Length of *p: 4
Length of *t: 1
*/

指针的算数运算

指针可以进行++、--、+、-等运算。

例:指针递增

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>

const int MAX = 3;

int main (){
int var[] = {10, 100, 200};
int i, *ptr;

/* 指针中的数组地址 */
ptr = var;
for ( i = 0; i < MAX; i++) {

printf("存储地址:var[%d] = %x\n", i, ptr );
printf("存储值:var[%d] = %d\n", i, *ptr );

/* 移动到下一个位置 */
ptr++;
}
return 0;
}

指针数组

可以通过指针来声明数组

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>

int main (){
const char *names[4] = { "Tom", "Bob", "Tony", "Jack" };

int i = 0;
while ( i < 4 ) {
printf("Value of names[%d] = %s\n", i, names[i] );
i++;
}
return 0;
}

指向指针的指针

指向指针的指针是一种多级间接寻址的形式,或者说是一个指针链。通常,一个指针包含一个变量的地址。当我们定义一个指向指针的指针时,第一个指针包含了第二个指针的地址,第二个指针指向包含实际值的位置。

例:

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
#include <stdio.h>

int main (){
int var;
int *ptr;
int **pptr;

var = 3000;

ptr = &var; // 取var的地址作为指针ptr的值

pptr = &ptr; // 取ptr的地址作为pptr的值

printf("&var(%x) => var(%d)\n", &var, var );
printf("&ptr(%x) => ptr(%x) => *ptr(%d)\n", &ptr, ptr,*ptr );
printf("&pptr(%x) => pptr(%x) => **pptr(%d)\n", &pptr, pptr, **pptr);

return 0;
}

/*
执行结果:
&var(ffe3b11c) => var(3000)
&ptr(ffe3b118) => ptr(ffe3b11c) => *ptr(3000)
&pptr(ffe3b114) => pptr(ffe3b118) => **pptr(3000)
*/

指针作为函数参数

当指针(地址)作为函数参数时,函数可以在执行过程中改变指针所指向值:
例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>
#include <time.h>

void getSeconds(unsigned long *par);

int main (){
unsigned long sec;
printf("[main] &sec(%x)=>sec(%ld): \n", &sec,sec);
getSeconds( &sec );
printf("[main] sec: %ld\n", sec );
return 0;
}

void getSeconds(unsigned long *par){
printf("[getSeconds] &par(%x)=>par(%x): \n", &par, par);
/* 获取当前的秒数 */
*par = time( NULL );
return;
}

指针作为函数返回值

指针作为函数返回值时,调用方可以直接访问这个地址:
例:

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
#include <stdio.h>

int * getList();

int main (){
int * i;
int j;
printf("[main] &i(%x) => i(%x)\n", &i,i);
i = getList();
printf("[main] &i(%x) => i(%x)\n", &i,i);
//for(j=0;j<5;j++)
// printf("%d\t", i[j]);
return 0;
}

int * getList(){
static int list[5]={1,2,3,4,5};
printf("[getList] &list(%x) => list(%x)\n", &list,list);
return list;
}

/*
执行结果:
[main] &i(ff94f03c) => i(5661b25b)
[getList] &list(5661e01c) => list(5661e01c)
[main] &i(ff94f03c) => i(5661e01c)
*/

数组

一维数组

1
2
int arr[] = {1,2,3,4,5};
char list[] = {"Tom", "Tony"};

多维数组

多维数组可以通过在括号内为每行指定值来进行初始化:

1
2
3
4
5
6
7
8
9
int a[3][4] = {  
{0, 1, 2, 3} , /* 初始化索引号为 0 的行 */
{4, 5, 6, 7} , /* 初始化索引号为 1 的行 */
{8, 9, 10, 11} /* 初始化索引号为 2 的行 */
};

// Or

int a[3][4] = {0,1,2,3,4,5,6,7,8,9,10,11};

多维数组的访问:

1
int val = a[2][3]; //获取数组中第2行第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
#include <stdio.h>

/* 函数声明 */
double getAverage(int arr[], int size);

int main (){
/* 带有 5 个元素的整型数组 */
int balance[5] = {1000, 2, 3, 17, 50};
double avg;

/* 传递一个指向数组的指针作为参数 */
avg = getAverage( balance, 5 ) ;

/* 输出返回值 */
printf( "平均值是: %f ", avg );

return 0;
}

double getAverage(int arr[], int size){
int i;
double avg;
double sum=0;

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

avg = sum / size;

return avg;
}

数组作为函数返回值

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
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

/* 要生成和返回随机数的函数 */
int * getRandom(){
static int r[10];
int i;

/* 设置种子 */
srand( (unsigned)time( NULL ) );
for ( i = 0; i < 10; ++i){
r[i] = rand();
printf( "r[%d] = %d\n", i, r[i]);
}

return r;
}

/* 要调用上面定义函数的主函数 */
int main (){
/* 一个指向整数的指针 */
int *p;
int i;

p = getRandom();
for ( i = 0; i < 10; i++ ) {
printf( "*(p + %d) : %d\n", i, *(p + i));
}

return 0;
}

指针和数组的关系

在C语言中,数组名是一个指向数组中第一个元素的常量指针,例如

1
double balance[50];

这个数组中,balance是一个指向&balance[0] 的指针,即数组 balance 的第一个元素的地址。

因此,把 p 赋值为 balance 的第一个元素的地址:

1
2
3
4
double *p;
double balance[10];

p = balance;

使用数组名作为常量指针是合法的,反之亦然。因此,*(balance + 4) 是一种访问 balance[4] 数据的合法方式。

把第一个元素的地址存储在 p 中,就可以使用 *p、*(p+1)、*(p+2)等来访问数组元素(也等同于*list+1)。

今晚还是在研究相对轻松的内容。主要涉及C语言的函数、作用域、预处理和头文件。

函数的声明

C语言中函数的定义:

1
2
3
4
return_type function_name( parameter list )
{
body of the function
}

C语言中函数的传值调用:

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
#include <stdio.h>

void swap(int x, int y)
{
int temp;

temp = x;
x = y;
y = temp;

return;
}
int main ()
{
/* 局部变量定义 */
int a = 100;
int b = 200;

printf("交换前,a 的值: %d\n", a );
printf("交换前,b 的值: %d\n", b );

swap(a, b);

printf("交换后,a 的值: %d\n", a );
printf("交换后,b 的值: %d\n", b );

return 0;
}

C语言函数的引用调用:

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
#include <stdio.h>

void swap(int *x, int *y)
{
int temp;
temp = *x; /* 保存地址 x 的值 */
*x = *y; /* 把 y 赋值给 x */
*y = temp; /* 把 temp 赋值给 y */

return;
}

int main ()
{
/* 局部变量定义 */
int a = 100;
int b = 200;

printf("交换前,a 的值: %d\n", a );
printf("交换前,b 的值: %d\n", b );

/* 调用函数来交换值
* &a 表示指向 a 的指针,即变量 a 的地址
* &b 表示指向 b 的指针,即变量 b 的地址
*/
swap(&a, &b);

printf("交换后,a 的值: %d\n", a );
printf("交换后,b 的值: %d\n", b );

return 0;
}

C语言可变参数的函数:

在C语言标准库中,stdarg.h头文件用于支持可变参数的函数,该文件提供了实现可变参数功能的函数和宏。具体步骤如下:
定义一个函数,最后一个参数为省略号,省略号前面可以设置自定义参数。
在函数定义中创建一个 va_list 类型变量,该类型是在 stdarg.h 头文件中定义的。
使用 int 参数和 va_start 宏来初始化 va_list 变量为一个参数列表。宏 va_start 是在 stdarg.h 头文件中定义的。
使用 va_arg 宏和 va_list 变量来访问参数列表中的每个项。
使用宏 va_end 来清理赋予 va_list 变量的内存。

具体操作如下:

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
#include <stdio.h>
#include <stdarg.h>

//一个求平均数的函数
double average(int num,...){
va_list valist;
double sum = 0.0;
int i;

/* 为 num 个参数初始化 valist */
va_start(valist, num);

/* 访问所有赋给 valist 的参数 */
for (i = 0; i < num; i++){
sum += va_arg(valist, int);
}
/* 清理为 valist 保留的内存 */
va_end(valist);

return sum/num;
}

int main(){
printf("Average of 2, 3, 4, 5 = %f\n", average(4, 2,3,4,5));
printf("Average of 5, 10, 15 = %f\n", average(3, 5,10,15));
}

函数的作用域

  1. 在函数或块内部声明的是的局部变量
    局部变量只能在函数内部使用。

  2. 在所有函数外部声明的是全局变量
    全局变量即可在函数内部使用,也可在函数外部使用。全局变量通常声明在程序的顶部,其在整个程序生命周期内都是有效的,在任意的函数内部能访问全局变量。

局部变量和全局变量的名称可以相同,但是在函数内,如果两个名字相同,会使用局部变量值,全局变量不会被使用。

全局变量与局部变量在内存中的区别:

全局变量保存在内存的全局存储区中,占用静态的存储单元;
局部变量保存在栈中,只有在所在函数被调用时才动态地为变量分配存储单元。

具体的例子:

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
#include <stdio.h>  
#include <stdlib.h>
int k1 = 1;
int k2;
static int k3 = 2;
static int k4;
int main(){
static int m1 = 2, m2;
int i = 1;
char*p;
char str[10] = "hello";
char*q = "hello";
p = (char *)malloc(100);
free(p);
printf("栈区-变量地址 i:%p\n", &i);
printf("栈区-变量地址 p:%p\n", &p);
printf("栈区-变量地址 str:%p\n", str);
printf("栈区-变量地址 q:%p\n", &q);
printf("堆区地址-动态申请:%p\n", p);
printf("全局外部有初值 k1:%p\n", &k1);
printf(" 外部无初值 k2:%p\n", &k2);
printf("静态外部有初值 k3:%p\n", &k3);
printf(" 外静无初值 k4:%p\n", &k4);
printf(" 内静态有初值 m1:%p\n", &m1);
printf(" 内静态无初值 m2:%p\n", &m2);
printf(" 文字常量地址:%p, %s\n", q, q);
printf(" 程序区地址:%p\n", &main);
return 0;
}

/*
执行结果:
栈区-变量地址 i:0xff9f9e8c
栈区-变量地址 p:0xff9f9e88
栈区-变量地址 str:0xff9f9e7e
栈区-变量地址 q:0xff9f9e78
堆区地址-动态申请:0x574ef160
全局外部有初值 k1:0x56644024
外部无初值 k2:0x5664403c
静态外部有初值 k3:0x56644028
外静无初值 k4:0x56644034
内静态有初值 m1:0x5664402c
内静态无初值 m2:0x56644038
文字常量地址:0x56642008, hello
程序区地址:0x566411b9
*/

  1. C语言的形参与实参

形参(形式参数)
在函数定义中出现的参数可以看做是一个占位符,它没有数据,只能等到函数被调用时接收传递进来的数据,所以称为形式参数,简称形参。
实参(实际参数)
函数被调用时给出的参数包含了实实在在的数据,会被函数内部的代码使用,所以称为实际参数,简称实参。

形参和实参的功能是传递数据,发生函数调用时,实参的值会传递给形参。

形参和实参的区别和联系
1) 形参变量只有在函数被调用时才会分配内存,调用结束后,立刻释放内存,所以形参变量只有在函数内部有效,不能在函数外部使用。

2) 实参可以是常量、变量、表达式、函数等,无论实参是何种类型的数据,在进行函数调用时,它们都必须有确定的值,以便把这些值传送给形参,所以应该提前用赋值、输入等办法使实参获得确定值。

3) 实参和形参在数量上、类型上、顺序上必须严格一致,否则会发生“类型不匹配”的错误。当然,如果能够进行自动类型转换,或者进行了强制类型转换,那么实参类型也可以不同于形参类型。

4) 函数调用中发生的数据传递是单向的,只能把实参的值传递给形参,而不能把形参的值反向地传递给实参;换句话说,一旦完成数据的传递,实参和形参就再也没有瓜葛了,所以,在函数调用过程中,形参的值发生改变并不会影响实参。

预处理和头文件

C语言的预处理器

C 预处理器相当于一个文本替换工具,会指示编译器在实际编译之前完成所需的预处理。C 预处理器(C Preprocessor)简写为 CPP。
所有的C预处理器命令都是以井号(#)开头。它必须是第一个非空字符,为了增强可读性,预处理器指令应从第一列开始。
下面列出了所有重要的预处理器指令:

指令 描述
#define 定义宏
#include 包含一个源代码文件
#undef 取消已定义的宏
#ifdef 如果宏已经定义,则返回真
#ifndef 如果宏没有定义,则返回真
#if 如果给定条件为真,则编译下面代码
#else #if 的替代方案
#elif 如果前面的 #if 给定条件不为真,当前条件为真,则编译下面代码
#endif 结束一个 #if……#else 条件编译块
#error 当遇到标准错误时,输出错误消息
#pragma 使用标准化方法,向编译器发布特殊的命令到编译器中

一些样例:

1
2
3
//这个指令告诉 CPP 取消已定义的 FILE_SIZE,并定义它为 42。
#undef FILE_SIZE
#define FILE_SIZE 35
1
2
3
4
//这个指令告诉 CPP 只有当 MESSAGE 未定义时,才定义 MESSAGE。
#ifndef MESSAGE
#define MESSAGE "You wish!"
#endif
1
2
3
4
5
6
7
#ifdef DEBUG
/* Your debugging statements here */
#endif
/*
这个指令告诉 CPP 如果定义了 DEBUG,则执行处理语句。在编译时,如果向 gcc 编译器传递了 -DDEBUG 开关量,这个指令就非常有用。
它定义了 DEBUG,可在编译期间随时开启或关闭调试。
*/

预定义宏

ANSI C 定义了许多宏。在编程中可以使用这些宏,但是不能直接修改这些预定义的宏。

描述
__DATE__ 当前日期,一个以 “MMM DD YYYY” 格式表示的字符常量。
__TIME__ 当前时间,一个以 “HH:MM:SS” 格式表示的字符常量。
__FILE__ 这会包含当前文件名,一个字符串常量。
__LINE__ 这会包含当前行号,一个十进制常量。
__STDC__ 当编译器以 ANSI 标准编译时,则定义为 1。

C语言头文件
头文件是扩展名为 .h 的文件,包含了 C 函数声明和宏定义,被多个源文件中引用共享。有两种类型的头文件:程序员编写的头文件和编译器自带的头文件。

只引用一次头文件

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

the entire header file file

#endif

有条件引用

有时需要从多个不同的头文件中选择一个引用到程序中。

例如,需要指定在不同的操作系统上使用的配置参数:

1
2
3
4
5
6
7
#if SYSTEM_1
# include "system_1.h"
#elif SYSTEM_2
# include "system_2.h"
#elif SYSTEM_3
...
#endif

吃完饭,洗过碗,开始第一天的活动。

今天的复习内容最轻松,因为是基础中的基础。主要涉及C语言的编译器、语法关键字、运算符、变量及数据类型。

编译器

C语言在Linux上的编译器:GCC

编译tips:使用GCC在64位电脑上编译32位程序

1
2
3
4
5
gcc -m32 -o test test.c

#先要安装build-essential module-assistant gcc-multilib g++-multilib

apt-get install build-essential module-assistant gcc-multilib g++-multilib

C语言在Windows上的编译器:CL.EXE
Windows上的集成开发环境,通常是Visual Studio,目前最新的已经是VS 2019版本了,与早些年不同的是,VS Community版本目前对个人是免费的。
但是因为一直以来的习惯,我还是习惯用VC6.0作为我的Windows C语言开发环境。编译连接仍然采用CL.EXE和LINK.EXE,以及NMAKE.EXE。

基本的编译语法

以Hello World为例,代码如下:

1
2
3
4
5

int main(){
printf("Hello World\n");
return 0;
}

Linux(Debian)下的编译过程
gcc -o helloworld helloworld.c

Windows下使用CL.EXE进行编译

1
2
cl.exe /c helloworld.c
link helloworld.obj /LIBPATH:C:\Tools\Binary\VC++6.0\VC98\Lib\

这里C:\Tools\Binary\VC++6.0\VC98\Lib\是我的VC6路径,并且执行cl和link需要将程序路径加入到环境变量。

语法关键字

1
2
3
4
5
6
7
auto  break case  char  const  
continue default do double else
enum extern float for goto
if int long register return
short signed sizeof static struct
switch typedef unsigned union void
volatile while

部分关键字含义

  • continue 结束当前循环,开始下一轮循环
  • extern 声明变量或函数是在其它文件或本文件的其他位置定义
  • goto 无条件跳转语句
  • register 声明寄存器变量
  • typedef 用以给数据类型取别名
  • volatile 说明变量在程序执行中可被隐含地改变

运算符

基本运算符

加减乘除、取模、自增、自减: +,-,*,/,%,++,–

赋值运算符

1
2
3
4
5
6
7
8
9
10
11
=:简单的赋值运算符,把右边操作数的值赋给左边操作数	C = A + B 将把 A + B 的值赋给 C
+=:加且赋值运算符,把右边操作数加上左边操作数的结果赋值给左边操作数 C += A 相当于 C = C + A
-=:减且赋值运算符,把左边操作数减去右边操作数的结果赋值给左边操作数 C -= A 相当于 C = C - A
*=:乘且赋值运算符,把右边操作数乘以左边操作数的结果赋值给左边操作数 C *= A 相当于 C = C * A
/=:除且赋值运算符,把左边操作数除以右边操作数的结果赋值给左边操作数 C /= A 相当于 C = C / A
%=:求模且赋值运算符,求两个操作数的模赋值给左边操作数 C %= A 相当于 C = C % A
<<=:左移且赋值运算符 C <<= 2 等同于 C = C << 2
>>=:右移且赋值运算符 C >>= 2 等同于 C = C >> 2
&=:按位与且赋值运算符 C &= 2 等同于 C = C & 2
^=:按位异或且赋值运算符 C ^= 2 等同于 C = C ^ 2
|=:按位或且赋值运算符 C |= 2 等同于 C = C | 2

逻辑运算符

1
与或非:&&,||,!

位运算符

1
与、或、异或、取反、左移、右移:&,|,^,~,<<,>>

杂项运算符

1
2
3
&	返回变量的地址
* 指向一个变量
? : 条件表达式三元运算符

变量及数据类型

基本类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
整数类型:
char 1 字节 -128 到 127 或 0 到 255
unsigned char 1 字节 0 到 255
signed char 1 字节 -128 到 127
int 2 或 4 字节 -32,768 到 32,767 或 -2,147,483,648 到 2,147,483,647
unsigned int 2 或 4 字节 0 到 65,535 或 0 到 4,294,967,295
short 2 字节 -32,768 到 32,767
unsigned short 2 字节 0 到 65,535
long 4 字节 -2,147,483,648 到 2,147,483,647
unsigned long 4 字节 0 到 4,294,967,295

浮点类型:
float 4 字节 1.2E-38 到 3.4E+38 6 位小数
double 8 字节 2.3E-308 到 1.7E+308 15 位小数
long double 16 字节 3.4E-4932 到 1.1E+4932 19 位小数

枚举类型

可以在定义枚举类型时改变枚举元素的值:

enum season {spring, summer=3, autumn, winter};

没有指定值的枚举元素,其值为前一元素加1。此例中spring的值为0,summer的值为 3,autumn 的值为4,winter 的值为5。

三种写法

  1. 先定义枚举类型,再定义枚举变量
1
2
3
4
5
enum DAY
{
MON=1, TUE, WED, THU, FRI, SAT, SUN
};
enum DAY day;
  1. 定义枚举类型的同时定义枚举变量
1
2
3
4
enum DAY
{
MON=1, TUE, WED, THU, FRI, SAT, SUN
} day;
  1. 省略枚举名称,直接定义枚举变量
1
2
3
4
enum
{
MON=1, TUE, WED, THU, FRI, SAT, SUN
} day;

引用枚举类型:

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>

enum DAY {
MON=1, TUE, WED, THU, FRI, SAT, SUN
};

int main() {
enum DAY day;
day = WED;
printf("%d",day);
return 0;
}

应该找点让自己开心的事情做。

C语言是我进入计算机领域学习的第一门语言。可惜工作这么多年,早已忘的支离破碎,对链表、数据结构乃至系统编程的知识,早已经还给老师了。这对今后的学习是非常不利的。所以计划从今天开始,抽空花30个晚上,每晚花1~2个小时的时间复习C语言,尽量保证连续。

我的复习会比较跳跃,这个系列的文章,也仅适合作为个人的技术笔记,不太适合作为入门资料进行参考。

我可能会在文章中提及一些调试、算法以及工程方面的技术,相信其中有些内容对搜索引擎来说会是一个比较好的长尾关键词,这样也给一些编程过程中遇到问题的朋友一些参考。本着少废话的原则,尽量避免探讨过于简单的技术。否则可能会沦为小白记事本,反而容易误导网友。

暂时给自己拟定了以下复习计划,30个夜晚主要这样安排(后续可能根据实际情况有所调整):

第一夜:编译器、语法关键字、运算符、变量及数据类型
第二夜:函数、作用域、预处理和头文件
第三夜:指针和数组
第四夜:存储类型、结构体、链表
第五夜:C语言标准库、错误处理和调试、输入输出
第六夜:内存管理、读写文件、管道
第七夜:线程、多线程
第八夜:进程、多进程
第九夜:C语言常见的Linux系统调用
第十夜:C语言常见的Windows系统调用
第十一夜:在C语言中使用内联汇编
第十二夜:使用C语言探索堆栈
第十三夜:网络编程1-网络协议及其数据结构
第十四夜:网络编程2-Socket通信
第十五夜:网络编程3-常见的应用层协议
第十六夜:编写一个Linux的服务
第十七夜:编写一个Windows的服务
第十八夜:
第十九夜:编写一个Linux用户态Trojan
第二十夜:编写一个Windows用户态Trojan
第二十一夜:Linux内核编程1-内核源代码分析
第二十二夜:Linux内核编程2-实现一个内核模块
第二十三夜:Linux内核编程3-实现一个块设备驱动
第二十四夜:Linux内核编程4-实现一个字符设备驱动
第二十五夜:Linux内核编程5-实现一个内核Trojan
第二十六夜:Linux内核编程5-在Linux内核中瞎搞
第二十七夜:Windows内核编程1-Windows内核初探
第二十八夜:Windows内核编程2-Windows驱动编程研究
第二十九夜:Windows内核编程3-在Windows内核中瞎搞
第三十夜:学习总结

真是一个宏大的计划,甚至不知道自己能不能完成,有点期待。

希望可以自律、坚持地完成下去。

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×