【笔记整理 - Linux环境C语言编程】

VC 和 Visual Studio 的界面开发都属于 MFC 。

Linux环境的C编程

先安装gcc软件。

yum install gcc -y

用vim编写代码(必须是.c后缀),然后编译

gcc -o 生成的文件名 源代码文件

需要修改的习惯

h.c: 在函数‘main’中:
h.c:4:2: 错误:只允许在 C99 模式下使用‘for’循环初始化声明
  for(int i = 0; i < 10; ++i)
  ^
h.c:4:2: 附注:使用 -std=c99 或 -std=gnu99 来编译您的代码

方法一:修改for循环

int i;
for(i = 0; ...)

方法二:修改编译命令

[root@localhost coding]# gcc -std=c99 -o h h.c

编译C++代码

先安装gcc-c++

yum install gcc-c++ -y

然后就能使用g++命令编译c++代码:

g++ -o 生成的文件名 源代码文件

C语言的输出输入

在还没有图形界面的时代,C语言的输入输出函数都很重要,现在C语言程序基本运行在后台,输入函数几乎没用了;输出函数的printf很重要。

对于初学者,学习scanf和printf就行了。

printf

整数——%d、字符——%c、浮点数——%f、字符串——%s

#include
int main()
{
 printf("%d\n%d\n%d\n",1,2,3);
}

需要额外注意的是输出字符串:

[root@localhost coding]# vi hello.c
#include
int main()
{
 char str[] = "lexington";
 printf("%d\n%d\n%d\n%s\n",1,2,3,str);
}

// 编译和执行都没问题
[root@localhost coding]# gcc -o hello hello.c
[root@localhost coding]# ./hello
1
2
3
lexington

按照教程的写法:

[root@localhost coding]# vi hello.c
#include
// !没有这个头文件会报错“隐式声明与内建函数‘memset’不兼容”
#include 	
int main()
{
 char str[10];
 
 // 重点!
 memset(str, 0, sizeof(str));
 strcpy(str, "lexington");
 
 printf("%d\n%d\n%d\n%s\n",1,2,3,str);
}

// 输出一样
[root@localhost coding]# gcc -o hello hello.c
[root@localhost coding]# ./hello
1
2
3
lexington

scanf

仅仅在学习时期有用

%lf——浮点数,其余与printf一样。

// 提供的是变量的地址,如果是数组就不需要“&”了
scanf("%s %c %d %lf",name,&xb,&age,&weight);

练习

#include
#include 
int main()
{
 char name[20];
 char sex = 0;
 int age = 0, high = 0;
 float weight = 0.0;
 char body[20];

 memset(name, 0, sizeof(name));
 memset(body, 0, sizeof(body));

 printf("依次输入您喜欢的女(男)神的姓名、性别(x-男,y-女)、年龄、身高(cm)、体重(kg)、和您最喜欢她的身体部位,用空格将各个参数
分隔开\n");

 scanf("%s %c %d %d %lf %s", name ,&sex, &age, &high, &weight, body);

 printf("\n");

// scanf("%d %f", &high, &weight);

 if(sex == 'x')
  printf("您最喜欢的男神是:%s。各项参数如下:\n", name);
 else if(sex == 'y')
  printf("您最喜欢的女神是:%s。各项参数如下:\n", name);
 printf("年龄:%d\n", age);
 printf("身高:%d\n", high);
 printf("体重:%f\n", weight);
 printf("最喜欢的身体部位:%s\n", body);
 
}

犯过的错误:

scanf输入数据时:像下方的写法,实际输入数据是就要用空格“ ”将各个参数隔开

scanf("%s %c %d %d %lf %s", name ,&sex, &age, &high, &weight, body);

另一种写法:前2个参数用“ ”隔开,后2个参数用“,”隔开。

scanf("%d %d,%s", &i, &j,str);
// 有效输入:“1 2,qwe”

问题2:

变量agehigh,单独赋值

scanf("%d %f", &high, &weight);

可以正常录入数据,但放在一起赋值:

scanf("%s %c %d %d %lf %s", name ,&sex, &age, &high, &weight, body);

就会显示无效字符,前面和后面的变量都能正常读入数据并显示。

问题出在%lf

%lf是用来读入double类型变量的,而%f才是对应float类型变量。

在使用printf时,doublefloat都是用%f输出;

在使用scanf时,%lf对应double%f对应float

数组

sizeof(数组名)能直接得出数组占用内存的大小。

数组的初始化

int no[10];
memset(no, 0, sizeof(no)); // 位置, 值, 空间大小

但似乎也可以使用熟悉的C++方式初始化:

int arr[5] = {0}; // 第一个元素指定为0,其余元素默认初始化为0。

数组和指针

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

 // 三者等价
 printf("%p\n", arr);
 printf("%p\n", &arr);
 printf("%p\n", &arr[0]);
}
[root@localhost coding]# ./test
0x7ffd73d9a990
0x7ffd73d9a990
0x7ffd73d9a990

字符串

\0结尾的char类型数组。

字符串的赋值

char strword[21];
memset(strword, 0, sizeof(strword));
strcpy(strword, "hello");

或通过下标运算符一个个字符赋值。

// 获取字符串(char数组)所占用的内存空间
sizeof(str);

// 获取字符串长度(不包括\0)
strlen(str);

函数

Linux环境下的编程,可以使用man指令查看库函数的详细信息。

在不知道库函数所属头文件时很有用

[root@localhost ~]# man strcpy

多文件编程

当把一些函数定义放在其它.c文件中时,要放在一起编译,如下所示。头文件在.c文件中include就行了。

[root@localhost coding]# gcc -o test test.c m.c

变量作用域

在C++中,可以使用::前缀表示使用全局名称。适用于变量和函数。

例如:

int i = 10;
int main()
{
	int i = 5;
	// 后者使用的是全局变量i
	printf("%d, %d \n", i, ::i); 
}
[root@localhost coding]# g++ -o test test.c 
[root@localhost coding]# ./test
5, 10

C语言不支持此功能,用gcc编译会出错

指针

指针的运算

指针能进行+-运算,运算结果的具体增量由指针所指的类型大小决定。

int main()
{
 char c = 'a';
 int i = 0;
 double d = 0.1;
 
 // 如果全部初始化为0,则无法得出有效的结果
 // 可以直接创建数组,更方便一些,例如:
 // char carr[1];
 // int iarr[1];
 // double darr[1];
 char* pc = &c;
 int* pi = &i;
 double* pd = &d;

 printf("%d, %p, %p \n", sizeof(*pc), pc, pc+1);
 printf("%d, %p, %p \n", sizeof(*pi), pi, pi+1);
 printf("%d, %p, %p \n", sizeof(*pd), pd, pd+1);
}
[root@localhost coding]# ./test
1, 0x7ffce242c117, 0x7ffce242c118 	增量1
4, 0x7ffce242c110, 0x7ffce242c114 	增量4
8, 0x7ffce242c108, 0x7ffce242c110	增量8

指针大小

64位系统大小为8Byte,32位系统中为4Byte。

int main()
{
 printf("%d \n", sizeof(int *));
}
[root@localhost coding]# ./test
8 

因为VS的影响,已经默认为指针是4Byte大小了。

整数

二进制——0b、八进制——0、十六进制——0x

0b和0x不分大小写,十六进制的字母部分也不分大小写

整数输出

%hd、%d、%ld
以十进制、有符号的形式输出short、int、long 类型的整数。

%hu、%u、%lu
以十进制、无符号的形式输出short、int、long 类型的整数。

===============

%ho、%o、%lo
以八进制、不带前缀、无符号的形式输出 short、int、long 类型的整数

%#ho、%#o、%#lo
以八进制、带前缀、无符号的形式输出   short、int、long 类型的整数

===============

%hx、%x、%lx
%hX、%X、%lX
以十六进制、不带前缀、无符号的形式输出 short、int、long 类型的整数。如果 x 小写,那么输出的十六进制数字也小写;如果 X 大写,那么输出的十六进制数字也大写。

%#hx、%#x、%#lx
%#hX、%#X、%#lX
以十六进制、带前缀、无符号的形式输出 short、int、long 类型的整数。如果 x 小写,那么输出的十六进制数字和前缀都小写;如果 X 大写,那么输出的十六进制数字和前缀都大写。

常用库函数

int  atoi(const char *nptr);   // 把字符串nptr转换为int整数
long atol(const char *nptr);     // 把字符串nptr转换为long整数
int  abs(const int j);            // 求int整数的绝对值
long labs(const long int j);     // 求long整数的绝对值

随机数

2个函数配合使用:srand()rand()

void srand(unsigned int seed);   
int  rand(); 

“C语言利用rand()函数取得随机数的时候是通过一个叫做“种子”的变量经过计算得出一个数值,然后得出的数值再作为“种子”参与下一次的运算,这样就得到了所谓的随机数,而srand()的作用就是用给定的数字来代替种子”

通常取系统时间作为种子数。因为每次运行程序的时间都不一样,得到的结果也不相同,使得随机函数更有随机性。

int main()
{
 int i;

 srand(time(0));

 for(i = 0; i < 5; ++i)
  printf("%d ", rand());

  printf("\n");
}

固定一个种子数后,每次运行程序输出的结果都是相同的。

int main()
{
 int i;

 for(i = 0; i < 5; ++i)
 {
  srand(0);
  printf("%d ", rand());
 }

  printf("\n");
}
[root@localhost coding]# ./test
1804289383 1804289383 1804289383 1804289383 1804289383 
[root@localhost coding]# ./test
1804289383 1804289383 1804289383 1804289383 1804289383
关于随机函数的结论

rand()函数的计算公式是固定的,相同的输入,相同的输出。如果确定了一个种子数,那么每次运行程序的结果都是相同的。

...
srand(0);
for(i = 0; i < 5; ++i)
	printf("%d ", rand());
...

运行结果:多次调用函数,得到的随机数序列相同。

[root@localhost coding]# ./test
1804289383 846930886 1681692777 1714636915 1957747793 
[root@localhost coding]# ./test
1804289383 846930886 1681692777 1714636915 1957747793
生成一定范围内的随机数

取模运算。

int i = rand() % 10;	// 0 ~ 9

int i = rand() % y + x;	// x ~ x+y-1

字符

常用函数

int isalpha(int ch);  // 若ch是字母('A'-'Z','a'-'z')返回非0值,否则返回0。
int isalnum(int ch);  // 若ch是字母('A'-'Z','a'-'z')或数字('0'-'9'),返回非0值,否则返回0。
int isdigit(int ch);  // 若ch是数字('0'-'9')返回非0值,否则返回0。
int islower(int ch);  // 若ch是小写字母('a'-'z')返回非0值,否则返回0。
int isupper(int ch);  // 若ch是大写字母('A'-'Z')返回非0值,否则返回0。
int tolower(int ch);  // 若ch是大写字母('A'-'Z')返回相应的小写字母('a'-'z')。
int toupper(int ch);  // 若ch是小写字母('a'-'z')返回相应的大写字母('A'-'Z')

不常用函数

int isascii(int ch);  // 若ch是字符(ASCII码中的0-127)返回非0值,否则返回0。
int iscntrl(int ch);  // 若ch是作废字符(0x7F)或普通控制字符(0x00-0x1F),返回非0值,否则返回0。
int isprint(int ch);  // 若ch是可打印字符(含空格)(0x20-0x7E)返回非0值,否则返回0。
int ispunct(int ch);  // 若ch是标点字符(0x00-0x1F)返回非0值,否则返回0。
int isspace(int ch);  // 若ch是空格(' '),水平制表符('/t'),回车符('/r'),走纸换行('/f'),垂直制表符('/v'),换行符('/n'),返回非0值,否则返回0。
int isxdigit(int ch); // 若ch是16进制数('0'-'9','A'-'F','a'-'f')返回非0值,否则返回0。

浮点数

浮点数float、double都是近似表示,位数越大,误差越明显。

实际开发中尽量使用double。

世纪开发中尽量使用整数替代浮点数,例如1.75米的身高,可以使用cm为单位,记录整数数据175。

int main()
{
 float f = 9999.9999;
 printf("%f \n", f);
 if( f == 9999.9999 )
  printf("%f \n", f);
}

输出结果:if判断为false

[root@localhost coding]# ./test 
10000.000000 

常用库函数

double atof(const char *nptr);       // 把字符串nptr转换为double
double fabs(double x);                // 求双精度实数x的绝对值
double pow(double x, double y);      // 求 x 的 y 次幂(次方)
double round(double x);               // double四舍五入
double ceil(double x);                // double向上取整数
double floor(double x);               // double向下取整数
double fmod(double x,double y);      // 求x/y整除后的双精度余数
// 把双精度val分解成整数部分和小数部分,整数部分存放在ip所指的变量中,返回小数部分。
double modf(double val,double *ip);

字符串

字符串的初始化最好还是用memset函数,其他方法可能会有隐患。

char str[10];
memset(str, 0, sizeof(str));

教程提到的不建议使用的方式是str[0] = 0;

char数组名可以当做const char *来使用。

常用库函数

获取长度

size_t strlen( const char* str);

字符串复制或赋值

char* strcpy(char* dest, const char* src);

// str长度=n:取前n个字符,结尾没有0
char* strncpy(char* dest, const char* src, const size_t n);

字符串拼接

// 拼接后结尾补上0

char* strcat(char* dest,const char* src);

char* strncat (char* dest,const char* src, const size_t n);

字符串比较

int strcmp(const char *str1, const char *str2 );

// 比较前n个字符形成的子串大小
int strncmp(const char *str1,const char *str2 ,const size_t n);

字符串查找

如果找不到,都返回0

// 结果指向第一个c出现的位置
char* strchr(const char *s,const int c);

// 结果指向最后一个c出现的位置
char* strrchr(const char *s,const int c);

// // 结果指向第一个子串出现的位置
char* strstr(const char* str,const char* substr);

做练习的一些收获

如果已知函数接收的某个指针实际上是数组名,则可以直接在函数内使用[],将该指针看做数组来操作。

似乎标准库没有提供截取字符串的操作,自己实现。

从字符串“path”中截取最后一个“/”之前的所有字符:

char* pLast = strrchr(path, '/');
char pPath[50];
memset(pPath, 0, sizeof(pPath));
strncpy(pPath, path, pLast - path);

类型转换

不同数据类型之间的差别在于数据的取值范围和精度上,一般情况下,数据的取值范围越大、精度越高,其类型也越“高级”。

整型类型级别从低到高依次为:

signed char->unsigned char->short->unsigned short->int->unsigned int->long->unsigned long

浮点型级别从低到高依次为:

float->double

操作数中没有浮点型数据时

当 char、unsigned char、short 或 unsigned short 出现在表达式中参与运算时,一般将其自动转换为 int 类型。

int 与 unsigned int混合运算时,int自动转换为unsigned int型。

int、unsigned int 与 long 混合运算时,均转换为 long 类型。

总结:int和long是2个分界线。

int以下的类型字段转换为int;

int之上,long之下的类型自动转换为long。

有符int自动转换为无符int

操作数中有浮点型数据时

当操作数中含有浮点型数据时,所有操作数都将转换为 double 型。

赋值运算符两侧的类型不一致时

当赋值运算符的右值(可能为常量、变量或表达式)类型与左值类型不一致时,将右值类型提升/降低为左值类型。

右值超出左值类型范围时

更糟糕的情况是,赋值运算符右值的范围超出了左值类型的表示范围,将把该右值截断后,赋给左值。所得结果可能毫无意义。

char c; 	//  char占8位,取值范围是-128-127。
c=1025; 	//  整数1025 对应二进制形式是100 0000 0001,超出了8位。
printf("%d",c) ;  //  以十进制输出c的值

输出结果为 1,因为只取 1025 低 8 位 0000 0001(值为1),赋给字符型变量 c,得到毫无意义的值。

结构体

理论上讲结构体的各个成员在内存中是连续存放的,和数组非常类似,但是,结构体的占用内存的总大小不一定等于全部成员变量占用内存大小之和。在编译器的具体实现中,为了提高内存寻址的效率,各个成员之间可能会存在缝隙

C语言提供了结构体成员内存对齐的方法,在定义结构体之前,增加以下代码可以使结构体成员变量之间的内存没有空隙。

#pragma pack(1)

函数声明、调用结构体的变量好像要加上struct关键字。例如:

void setvalue(struct st_girl *pst);

int main()
{
  struct st_girl queen;  // 定义结构体变量
  // 初始化结构体变量
  memset(&queen,0,sizeof(struct st_girl));
 
  setvalue(&queen);  // 调用函数,传结构体的地址
 
  printf("姓名:%s,年龄:%d\n",queen.name,queen.age);
}

有时又不用加。

结构体复制 memcpy

void *memcpy(void *dest, const void *src, size_t n);

C语言好像没有重载运算符。

枚举和共同体

应该是enum和union吧,作者表示极少用到,干脆不提了。

格式化输出

%[flags][width][.prec]type
type

详见“整数输出”部分。

width

控制输出内容的宽度

printf("=%12s=\n","abc");     // 输出=         abc=
printf("=%12d=\n",123);       // 输出=         123=
printf("=%12lf=\n",123.5);    // 输出=  123.500000=
flags

控制输出内容的对齐方式。

+:右对齐,缺省

-:左对齐

左对齐,小数点后会加0填充

printf("=%-12s=\n","abc");    // 输出=abc         =
printf("=%-12d=\n",123);     // 输出=123         =
printf("=%-12f=\n",123.5);    // 输出=123.500000  =

如果输出的内容是整数或浮点数,并且对齐的方式是右对齐,可以加0填充,例如:

  printf("=%012s=\n","abc");  // 输出=         abc=
  printf("=%012d=\n",123);   // 输出=000000000123=
  printf("=%012f=\n",123.5);  // 输出=00123.500000=

补齐:只有整数和浮点数能用0填充,浮点数最多可以补到6位。

prec

如果输出的内容是浮点数,它用于控制输出内容的精度,也就是说小数点后面保留多少位,后面的数四舍五入。

printf("=%12.2lf=\n",123.5);   // 输出=      123.50=
printf("=%.2lf=\n",123.5);     // 输出=123.50=
printf("=%12.2e=\n",123500000000.0);  // 输出=    1.24e+11=
printf("=%.2e=\n",123500000000.0);    // 输出=1.24e+11=

格式化输出到字符串!!

int printf(const char *format, ...);
int sprintf(char *str, const char *format, ...);
int snprintf(char *str, size_t size, const char *format, ...);

后2个函数是将格式化输出的内容保存到字符串str中。snprintfn类似之前字符串库函数的n版本,只获取输出结果的前n-1个字符。

C语言没有提供把整数和浮点数转换为字符串的库函数,而是采用sprintf和snprintf函数格式化输出到字符串。

int main()
{
  char str[301];
 
  // 格式化输出到str中
  sprintf(str,"%d,%c,%f,%s",10,'A',25.97,"一共输入了三个数。");
  printf("%s\n",str);
 
  // 格式化输出到str中,只截取前7个字符
  snprintf(str,8,"%d,%c,%f,%s",10,'A',25.97,"一共输入了三个数。");
  printf("%s\n",str);
}

C语言代码的多行书写

在一行代码的行尾放置一个反斜杠,c语言编译器会忽略行尾的换行符,而把下一行的内容也算作是本行的内容。这里反斜杠起到了续行的作用。

	strcpy(str,"aaaaaaaaaa\
bbbbbbbbb");

C语言编译器会自动连接字符串

下方代码三者等价。

strcpy(str,"aaabbbccc");
printf("str=%s=\n",str);   // 输出str=aaabbbccc=

strcpy(str,"aaa"\
	"bbb"\
	"ccc");
printf("str=%s=\n",str);   // 输出str=aaabbbccc=

// !!!!仅适用于 “字符串字面量” 
strcpy(str,"aaa""bbb""ccc");
printf("str=%s=\n",str);   // 输出str=aaabbbccc=

!!编程技巧

格式化输出的课后练习:解析XML格式数据。一共需要解析出string、int、double 3种类型参数的版本。

例子:

西施1816845.50火辣
自己的代码(int版本)
int GetXMLBuffer_Int(const char* in_XMLBuffer, const char* in_FieldName, int* out_Value)
{
	char frontTag[20];
	char rearTag[20];
	memset(frontTag, 0, sizeof(frontTag));
	memset(rearTag, 0, sizeof(rearTag));

	sprintf(frontTag, "<%s>", in_FieldName);
	sprintf(rearTag, "", in_FieldName);

	// 如果frontTag和rearTag返回空,则表示查找失败
	if (strlen(frontTag) == 0 || strlen(rearTag) == 0)
		return -1;

	const char* pTagBegin = strstr(in_XMLBuffer, frontTag) + strlen(frontTag);
	const char* pTagEnd = strstr(in_XMLBuffer, rearTag);

	char variable[20];
	memset(variable, 0, sizeof(variable));
	strncpy(variable, pTagBegin, pTagEnd - pTagBegin);

	*out_Value = atoi(variable);

	return 0;
}
参考答案

下方代码为string版本int版本double版本都是直接调用string版本,将得到的字符串结果转换为需要的类型就行了。

int GetXMLBuffer_Str(const char* in_XMLBuffer, const char* in_FieldName, char* out_Value)
{
	if (out_Value == 0) return -1;  // 如果out_Value是空地址,返回失败。

	char* start = 0, * end = 0;
	char m_SFieldName[51], m_EFieldName[51];  // 字段的开始和结束标签。

	int m_NameLen = strlen(in_FieldName);  // 字段名长度。
	memset(m_SFieldName, 0, sizeof(m_SFieldName));
	memset(m_EFieldName, 0, sizeof(m_EFieldName));

	snprintf(m_SFieldName, 50, "<%s>", in_FieldName);
	snprintf(m_EFieldName, 50, "", in_FieldName);

	start = 0; end = 0;
	start = (char*)strstr(in_XMLBuffer, m_SFieldName);  // 字段开始标签的位置
	if (start != 0)
		end = (char*)strstr(start, m_EFieldName);  // 字段结束标签的位置。
	if ((start == 0) || (end == 0)) return -1;

	int   m_ValueLen = end - start - m_NameLen - 2;  // 字段值的长度。

	strncpy(out_Value, start + m_NameLen + 2, m_ValueLen);  // 获取字段的值。

	out_Value[m_ValueLen] = 0;

	return 0;
}
int GetXMLBuffer_Int(const char *in_XMLBuffer,const char *in_FieldName,int *out_Value)
{
	char strvalue[51];
	memset(strvalue,0,sizeof(strvalue));
				if(GetXMLBuffer_Str(in_XMLBuffer,in_FieldName,strvalue)!=0) 
	return -1;

	(*out_Value)=atoi(strvalue);

	return 0;
}
收获

一、错误结果的返回点

自己的错误返回设置的不好:

	// 如果frontTag和rearTag返回空,则表示查找失败
	if (strlen(frontTag) == 0 || strlen(rearTag) == 0)
		return -1;

只有函数执行出错时才会返回错误,并没有检测到可能会导致错误的输入,所以基本上没有意义。

推荐答案的错误返回:

1、输入数据为空

	if (out_Value == 0) return -1;  // 如果out_Value是空地址,返回失败。

2、输入数据错误

	start = 0; end = 0; // 编程习惯,使用指针前都先置0
	start = (char*)strstr(in_XMLBuffer, m_SFieldName);  // 字段开始标签的位置
	if (start != 0)
		end = (char*)strstr(start, m_EFieldName);  // 字段结束标签的位置。
	if ((start == 0) || (end == 0)) return -1;

二、C语言的任一字符串的拼接使用sprintf函数

int sprintf(char *str, const char *format, ...);
int snprintf(char *str, size_t size, const char *format, ...);

C++:

string str1 = "hello";
string str2 = ",";
string str3 = "world";
string str4 = "!";
string str = str1 + str2 + str3 + str4;
std::cout << str << std::endl;

C:

char str1[] = "hello";
char str2[] = ",";
char str3[] = "world";
char str4[] = "!";
char str[20];
sprintf(str, "%s%s%s%s", str1, str2, str3, str4);
printf("%d\n", str);

2部分代码输出结果相同。

main函数的参数

linux系统的命令基本上都是C程序,大多数的程序都是需要参数的。

cp  book1.c book2.c
mkdir /tmp/dname
mv book3 /tmp/dname/book3
rm -rf /tmp/dname

极少数命令不带参数

clear
pwd

main函数有三个参数,argc、argv和envp,它的标准写法如下:

int main(int argc, char *argv[], char *envp[])

int argc,存放了命令行参数的个数。

char *argv[],是个字符串的数组,每个元素都是一个字符指针,指向一个字符串,即命令行中的每一个参数。

char *envp[],也是一个字符串的数组,这个数组的每一个元素是指向一个环境变量的字符指针。

argc和argv

例子:

#include
int main(int argc, char* argv[])
{
	printf("the number of parament : %d\n", argc);

	int i;
	for(i = 0; i < argc; ++i )
		printf("the detail of %d parament : %s\n", i, argv[i]);
}


[root@localhost coding]# ./test 1 2 3 4 5
the number of parament : 6
the detail of 0 parament : ./test
the detail of 1 parament : 1
the detail of 2 parament : 2
the detail of 3 parament : 3
the detail of 4 parament : 4
the detail of 5 parament : 5

注意:

1)argc的值是参数个数加1,因为程序名称是程序的第一个参数,即argv[0],在上面的示例中,argv[0]是./test

2)main函数的参数,不管是书写的整数还是浮点数,全部被认为是字符串。

3)参数的名称是约定俗成的,虽然可以改,但不建议。

C程序的规范写法

假设main函数都有参数,使用者如何知道参数的个数和含义?

作者推荐:

在main函数中做出分支判断,如果输入的参数数量、格式有误,输出错误信息。

例子:

/*
 * 程序名:book103.c,此程序演示main函数的参数。
 * 作者:C语言技术网(www.freecplus.net) 日期:20190525
*/
#include 
 
int main(int argc,char *argv[])
{
  if (argc!=6)
  {
    printf("\n这是一个超女选秀程序,根据提供的超女信息,判断"\
           "她是否符合王妃的标准。\n\n");
    printf("用法:./book103 name age height sc yz\n");
    printf("例如:./book103 西施 22 170 火辣 漂亮\n");
    printf("name   超女的姓名。\n");
    ...
    return -1;
  }
 
  printf("您输入的超女信息是:姓名(%s),年龄(%s),身高(%s),身材(%s),颜值(%s)。\n",\argv[1],argv[2],argv[3],argv[4],argv[5]);
  printf("正在计算中,请稍候......\n");
  ...
  return 0;
  }
}

这么做的原因:

1)程序的使用者不一定会写程序,也没必要去查使用手册等资料;

2)程序的使用者就算会写程序,也没必要在使用的时候去看源代码,并且,您也不一定想让他看到源代码;

3)如果程序的使用者是您自己,时间一长,您也会忘记程序的参数。

envp

envp存放了当前程序运行环境的参数。

#include
int main(int argc, char* argv[], char* envp[])
{
	int i = 0;
	while(envp[i] != 0)
	{
		printf("%s\n", envp[i]);
		++i;
	}
}

运行结果与linux好env命令输出相同。

[root@localhost coding]# ./test 
XDG_SESSION_ID=16
HOSTNAME=localhost.localdomain
SELINUX_ROLE_REQUESTED=
TERM=xterm
SHELL=/bin/bash
HISTSIZE=1000
SSH_CLIENT=192.168.0.196 63887 22
SELINUX_USE_CURRENT_RANGE=
SSH_TTY=/dev/pts/2
USER=root
LS_COLORS=rs=0:di=01;34:ln=01;36:mh=00:pi=40;33:so=01;35:do=01;35:bd=40;33;01:cd=40;33;01:or=40;31;01:mi=01;05;37;41:su=37;41:sg=30;43:ca=30;41:tw=30;42:ow=34;42:st=37;44:ex=01;32:*.tar=01;31:*.tgz=01;31:*.arc=01;31:*.ar...
...一大堆内容...

在实际开发中,envp参数的应用场景不多,各位了解一下就行了。

动态内存管理

相关的库函数

malloc

void *malloc(unsigned int size);

向系统申请一块大小为size的连续内存空间,如果申请失败,函数返回0,如果申请成功,返回成功分配内存块的起始地址。

返回值的地址的基类型为 void,即不指向任何类型的数据,只提供一个地址。

free

void free(void *p);

free的作用是释放指针p指向的动态内存空间,p是调用malloc函数时返回的地址,free函数无返回值。

野指针

野指针就是无效的指针,与空指针不同,野指针无法通过简单地判断是否为 NULL避免

1、内存指针变量未初始化

指针变量刚被创建时不一定会自动初始化成为空指针(与编译器有关),它的缺省值是可能随机的。所以,指针变量在创建的同时应当被初始化,要么将指针的值设置为0,要么让它指向合法的内存。

int *pi = 0;
或
int i;
int *pi = &i;

2、内存释放后之后指针未置空

调用free函数把指针所指的内存给释放掉,但指针不一定会赋值 0(也与编译器有关),如果对释放后的指针进行操作,相当于非法操作内存。释放内存后应立即将指针置为0。

free(pi);
pi=0;

文件操作

文本文件和二进制文件

文本数据

文本数据由字符串组成,存放了每个字符的 ASCII 码值,每个字符占一个字节,每个字节存放一个字符。

二进制数据

二进制数据是字节序列,数字123的二进制表示是01111011,如果用二进制格式形式存储,字符、短整型、短整型、长整型都可以存储123:

1)字符型一个字节
01111011

2)短整型2个字节
00000000 01111011

3)整型4个字节
00000000 00000000 00000000 01111011

4)长整型8个字节
00000000 00000000 00000000 00000000 00000000 00000000 00000000 01111011

文本文件可以用vi和记事本打开,看到的都是ASCII字符。二进制文件用记事本打开看到的是乱码,没有阅读意义。

文件的打开和关闭

C语言中对文件进行操作,必须先“打开”文件,操作(读和写)完成后,再“关闭”文件。


文件指针

操作文件的时候,C语言为文件分配一个信息区,该信息区包含文件描述信息、缓冲区位置、缓冲区大小、文件读写到的位置等基本信息。

这些信息用一个结构体来存放(struct _IO_FILE),这个结构体有一个别名FILE(typedef struct _IO_FILE FILE),FILE结构体和对文件操作的库函数在 stdio.h 头文件中声明。

打开文件的时候,fopen函数中会动态分配一个FILE结构体大小的内存空间,并把FILE结构体内存的地址作为函数的返回值,程序中用FILE结构体指针存放这个地址。

关闭文件的时候,fclose函数除了关闭文件,还会释放FILE结构体占用的内存空间。

FILE结构体指针习惯称为文件指针。


打开文件 fopen

库函数fopen来创建一个新的文件或者打开一个已存的文件。

FILE* fopen(const char* filename, const char* mode);

函数失败时返回0。

调用fopen打开文件的时候,一定要判断返回值,如果文件不存在、或没有权限、或磁盘空间满了,都有可能造成打开文件失败。

filename:字符串,要打开文件的 相对/绝对路径。

mode:字符串,指定文件打开的模式。

字符串 说明
r 只读方式打开文件,该文件必须存在。
r+ 读/写方式打开文件,该文件必须存在。
rb+ 读/写方式打开一个二进制文件,只允许读/写数据。
rt+ 读/写方式打开一个文本文件,允许读和写。
w 打开只写文件,若文件存在则文件长度清为零,即该文件内容会消失;若文件不存在则创建该文件。
w+ 打开可读/写文件,若文件存在则文件长度清为零,即该文件内容会消失;若文件不存在则创建该文件。
a 以附加的方式打开只写文件。若文件不存在,则会创建该文件;如果文件存在,则写入的数据会被加到文件尾后,即文件原先的内容会被保留(EOF 符保留)。
a+ 以附加方式打开可读/写的文件。若文件不存在,则会创建该文件,如果文件存在,则写入的数据会被加到文件尾后,即文件原先的内容会被保留(EOF符不保留)。
wb 只写方式打开或新建一个二进制文件,只允许写数据。
wb+ 读/写方式打开或新建一个二进制文件,允许读和写。
wt+ 读/写方式打开或新建一个文本文件,允许读和写。
at+ 读/写方式打开一个文本文件,允许读或在文本末追加数据。
ab+ 读/写方式打开一个二进制文件,允许读或在文件末追加数据。

在Linux平台下,打开文本文件和二进制文件的方式没有区别。

在windows平台下,如果以“文本”方式打开文件,当读取文件的时候,系统会将所有的**"\r\n"转换成"\n";当写入文件的时候,系统会将"\n"转换成"\r\n"**写入, 如果以"二进制"方式打开文件,则读和写都不会进行这样的转换。


关闭文件 fclose

fclose库函数用于关闭文件,函数的原型:

int fclose(FILE* fp);

fp为fopen函数返回的文件指针。

注意文件指针的存在,它从系统动态分配了内存,所以在调用fopen打开了一个文件后,一定要调用fclose关闭文件指针,释放内存。

还要注意调用fclose关闭文件指针时,传入的参数不能是野指针、空指针。


文件的读写

向文件中写数据 fprintf

可以理解为重定向了标准输出

C语言向文件中写入数据库函数有fputc、fputs、fprintf,在实际开发中,fputc和fputs没什么用,只介绍fprintf就可以了。fprintf函数的声明如下:

int fprintf(FILE* fp, const char* format, ...);

fprintf函数的用法与printf相同,只是多了第一个参数文件指针,表示把数据输出到文件。

程序员不必关心fprintf函数的返回值。

例子:

#include

int main(int argc, char* argv[], char* envp[])
{
	int i = 0;
	FILE* fp = 0;

	if((fp = fopen("/root/coding/iofile.txt", "w")) == 0)
	{
		printf("文件打开失败,返回-1\n");
		return -1;
	}

	for(i = 0; i < 5; ++i)
		fprintf(fp, "parament %d\n", i+1);

	fclose(fp);
}

注意!if判断部分容易写成下列形式:

if(fp = fopen("/root/coding/iofile.txt", "w") == 0)

==的优先级大于=,会报错:

test.c:9:8: 警告:赋值时将整数赋给指针,未作类型转换 [默认启用]

运行结果:

[root@localhost coding]# ./test 
[root@localhost coding]# ls
iofile.txt  test  test.c
[root@localhost coding]# cat iofile.txt 
parament 1
parament 2
parament 3
parament 4
parament 5

因为使用的是“w”模式,每次在写入新数据之前,都会将文件内容清空,所以无论执行多少次./testcat iofile.txt 的结果都不会改变。


从文件中读数据 fgets

C语言从文件中读取数据的库函数有fgets、fgetc、fscanf,在实际开发中,fgetc和fscanf没什么用,只介绍fgets就可以了。fgets函数的原型如下:

char* fgets(char* buf, int size, FILE* fp);

fgets的功能是从文件中读取一行。

**buf:**一个字符串,用于保存从文件中读到的数据。

**size:**是打算读取内容的长度。

**fp:**是待读取文件的文件指针。

如果文件中将要读取的这一行的内容的长度小于size,fgets函数就读取一行,如果这一行的内容大于等于size,fgets函数就读取size-1字节的内容。(第size个位置由\0结尾)

遇到了换行符或读到了文件尾,函数都会返回;只有读到了文件尾才会返回0。

调用fgets函数如果成功的读取到内容,函数返回buf,如果读取错误或文件已结束,返回空,即0。如果fgets返回空,可以认为是文件结束而不是发生了错误,因为发生错误的情况极少出现。

例子:

#include
#include

int main(int argc, char* argv[], char* envp[])
{
	FILE* fp = 0;
	char line[100];

	if ((fp = fopen("/root/coding/iofile.txt", "r")) == 0)
	{
		printf("文件打开失败,返回-1\n");
		return -1;
	}

	while (1)
	{
		memset(line, 0, sizeof(line));
		if (fgets(line, 5, fp) == 0)
			break;
		printf("%s", line);
	}

	fclose(fp);
}
[root@localhost coding]# ./test 
0123456789
abcdefghik
9876543210

疑问:将

printf("%s", line);

改为

printf("%s\n", line);

后,得到下方结果:

[root@localhost coding]# ./test 
0123
4567
89

abcd
efgh
ik

9876
5432
10

已解决:"如果一行剩下的字符数量小于size,fgets会将剩下的字符读完,而不会取下一行的字符。"

每次读取完5-1=4个字符,都会额外加上换行符'\n'

原文自带'\n'标记,使3段数据末都空了一行是原文自带的’\n’加上printf("%s\n", line);共同作用导致的。

不管 size 的值多大,fgets函只读取一行数据,不能跨行。


二进制文件的读写

二进制文件没有行的概念,没有字符串的概念。

我们把内存中的数据结构直接写入二进制文件,读取的时候,也是从文件中读取数据结构的大小一块数据,直接保存到数据结构中。注意,这里所说的数据结构不只是结构体,是任意数据类型。

向文件中写入数据 fwrite

fwrite函数用来向文件中写入数据块,它的原型为:

size_t fwrite(const void* ptr, size_t size, size_t count, FILE* stream);

参数的说明:

ptr:内存区块的指针,存放了要写入的数据的地址,它可以是数组、变量、结构体等。

**size:**要写入的一个单位的字节数。

**count:**要写入的单位数。

写入的总字节大小就等于 size * count。

**fp:**表示文件指针。

函数的返回值是本次成功写入数据的字节数,一般情况下,程序员不必关心fwrite函数的返回值。

例子:

#include
#include

struct MyClass
{
    char c;
    int i;
    long l;
    char str[10];
};

int main(int argc, char* argv[], char* envp[])
{
    struct MyClass mClass;
    FILE* fp = 0;

    if ((fp = fopen("/root/coding/iofile.txt", "w")) == 0)
    {
        printf("文件打开失败,返回-1\n");
        return -1;
    }

    mClass.c = 'a';
    mClass.i = 10;
    mClass.l = 100;
    strcpy(mClass.str, "hello");
    fwrite(&mClass, sizeof(mClass), 1, fp);

    fclose(fp);
}

保存的结果:

[root@localhost coding]# ./test 
[root@localhost coding]# vim iofile.txt 
aUf%
^@^@^@d^@^@^@^@^@^@^@hello^@^@^@ ^E@^@^@^@^@^@

可以看到很多乱码,其实并不是文件的内容乱,而是vi无法识别文件的格式,把内容当成ASCII码显示,如果内容刚好是ASCII码,就能正确显示,如果不是ASCII码(如年龄和身高是整数),就无法正常显示了。

注意:尝试过在上一节文本文件的写入例子基础上做修改:将fprintf改为使用fwrite。结果失败,输出的结果与文本文件无异。在使用了结构体后,才能得到乱码。


从文件中读取数据 fread

fread函数用来从文件中读取数据块,它的原型为:

size_t fread(void* ptr, size_t size, size_t count, FILE* fp);

**ptr:**用于存放从文件中读取数据的变量地址,它可以是数组、变量、结构体等。

**size:**固定填1。

**count:**表示打算读取的数据的字节数。

**fp:**表示文件指针。

调用fread函数如果成功的读取到内容,函数返回读取到的内容的字节数,如果读取错误或文件已结束,返回空,即0。如果fread返回空,可以认为是文件结束而不是发生了错误,因为发生错误的情况极少出现。

例子:

#include
#include

struct MyClass
{
    char c;
    int i;
    long l;
    char str[10];
};

int main(int argc, char* argv[], char* envp[])
{
    struct MyClass mClass;
    FILE* fp = 0;

    if ((fp = fopen("/root/coding/iofile.txt", "rb")) == 0)
    {
        printf("文件打开失败,返回-1\n");
        return -1;
    }

    while (1)
    {
        if (fread(&mClass, sizeof(mClass), 1, fp) == 0)
            break;
        printf("c = %c, i = %d, l = %d, str = %s \n", mClass.c, mClass.i, mClass.l, mClass.str);
    }

    fclose(fp);
}

输出结果:

[root@localhost coding]# gcc -o test0 test0.c
[root@localhost coding]# ./test0
c = a, i = 10, l = 100, str = hello

注意事项!

1、fwrite和fread函数也可以写入和读取文本文件但是没有换行的概念,不管是换行符或其它的特殊字符,无区别对待。

2、一般来说,二进制文件有约定的数据格式,程序必须按约定的格式写入/读取数据。

例子中test.c写入的是MyClass结构体,test0.c就要用MyClass结构体来存放读取到的数据。

这道理就像图片查看软件无法打开音频文件,音频播放软件也无法打开图片文件,因为音频文件和图片文件的格式不同。


文件定位

在文件内部有一个位置指针,用来指向文件当前读写的位置。在文件打开时,如果打开方式是r和w,位置指针指向文件的第一个字节,如果打开方式是a,位置指针指向文件的尾部。

每当从文件里读取n个字节或文件里写入n个字节后,位置指针也会向后移动n个字节。

文件位置指针与C语言中的指针不是一回事。位置指针仅仅是一个标志,表示文件读写到的位置,不是变量的地址。文件每读写一次,位置指针就会移动一次,不需要在程序中定义和赋值,而是由系统自动设置,对程序员来说是隐藏的。

在实际开发中,偶尔需要移动位置指针,实现对指定位置数据的读写。移动位置指针称为文件定位。

C语言提供了ftell、rewind和fseek三个库函数来实现文件定位功能。

ftell函数

ftell函数用来返回当前文件位置指针的值,这个值是当前位置相对于文件开始位置的字节数。

long ftell(FILE* fp);
rewind函数

rewind函数用来将位置指针移动到文件开头。声明如下:

void rewind(FILE* fp);
fseek函数

fseek() 用来将位置指针移动到任意位置,它的声明如下:

int fseek(FILE* fp, long offset, int origin);

**fp:**文件指针,也就是被移动的文件。

**offset:**偏移量,要移动的字节数。之所以为 long 类型,是希望移动的范围更大,能处理的文件更大。

offset 为正时,向后移动;offset 为负时,向前移动。

**offset向文件尾方向偏移时:**无论是否超出文件尾,都返回0;文件指针指向正确位置或文件尾;

**offset向文件头方向偏移时:**超出文件头——返回-1,文件指针不动;未超出文件头——返回0,文件指针指向正确位置;。

**origin:**起始位置,从何处开始计算偏移量。

C语言规定的起始位置有三种,分别为:

0-文件开头;

1-当前位置;

2-文件末尾。

例子:

fseek(fp,100,0);     // 从文件的开始位置向后移动100字节。
fseek(fp,100,1);     // 从文件的当前位置向后移动100字节。
fseek(fp,-100,2);    // 从文件的尾部位置向前移动100字节。

文件缓冲区

操作系统中,存在一个内存缓冲区,当调用fprintf、fwrite等函数往文件写入数据的时候,数据并不会立即写入磁盘文件,而是先写入缓冲区,等缓冲区的数据满了之后才写入文件。

还有一种情况就是程序调用了fclose时也会把缓冲区的数据写入文件。

在实际开发中,如果程序员想把缓冲区的数据立即写入文件,可以调用fflush库函数,它的声明如下:

int fflush(FILE* fp);

fp为文件指针。函数成功,返回0,其它失败。

标准输入/输出/错误

Linux操作系统为每个程序默认打开三个文件,即标准输入stdin、标准输出stdout和标准错误输出stderr。

其中0就是stdin,表示输入流,指从键盘输入;

1代表stdout,2代表stderr,1,2默认是显示器。

在实际开发中,一般会关闭这几个文件指针。

课后练习

数据输入遇到问题:

char in_c = 0;
int in_i = 0;
long in_l = 0;
char in_str[10] = {0};
...
scanf("%c %d %d %s", &in_c, &in_i, &in_l, in_str);
...
printf("%c %d %d %s\n", in_c, in_i, in_l, in_str);
[root@localhost coding]# ./test
输入MyClass结构的数据:char int long string,数据间用空格分隔
'a' 1 4 "hello"
' 0 0 
写入文件成功,可继续输入数据。第一个参数输入“-1”退出
a 0 0 
写入文件成功,可继续输入数据。第一个参数输入“-1”退出
' 1 4 "hello"
写入文件成功,可继续输入数据。第一个参数输入“-1”退出
一:吸收回车符

用scanf读取键盘输入时,按下回车键也会记为一个字符,所以在获取%c类型数据时,要用%*c吸收回车符

scanf("%c %c %c", &c1, &c2, &c3);
// 注意2者区别,下方代码吸收了回车符
scanf("%c %c %c%*c", &c1, &c2, &c3);
二:C语言的scanf行为与C++编程内的行为不同

在控制台输入scanf的参数时,判定非常严格,敲一个键位就是一个字符:

例如:'a'是3个字符,-1是2个字符

负责输入输出部分的代码:

scanf("%c %c %c%*c", &i1, &i2, &i3);
printf("output:%c,%c,%c.\n\n", i1, i2, i3);
// 这个没问题
[root@localhost coding]# ./test0
1 2 3
output:1,2,3.

// 共往缓冲区塞入了3*3+2(空格)11个字符,因为使用“%*c”吸收了3个“字符”之间的空格,所以输出看起来还是对齐的。
[root@localhost coding]# ./test0
'a' 'b' 'c'
output:',a,'.

output:',b,'.

output:',c,'.
// 如果没吸收空格:
[root@localhost coding]# ./test0
'a''b''c'
output:',a,'.

output:b,','.

1 2 3
output:',1,2.
三、sizeof

C语言没有类,直接用结构名传入struct作为参数时,要加上struct关键词

struct Entity
{ ... };
...
sizeof(Entity);	// 错误,会显示“Entity”未声明
sizeof(struct Entity);	// 正确
四、文件指针的移动

如果一开始将指针移到尾部,要求出文件信息单位数量;则在开始读取信息之前,要将文件指针移回起始位置。

五、马虎

练习中有按XML格式存储数据和读取数据2个练习,在存储数据时,一个XML标记没有写好:...,正确的写法应该是...

因为这一错误没有发现,导致在写读取代码时花了1个半小时来排错。

Linux目录操作

#include 

获取目录 getcwd

在C程序中调用getcwd函数可以获取当前的工作目录。

char* getcwd(char* buf,size_t size);

如果执行正确,返回值、第一个参数都可以获得结果。

getcwd函数把当前工作目录存入buf中,如果目录名超出了参数size长度,函数返回NULL,如果成功,返回buf。

例:

#include
#include
int main()
{
	char dir[50] = {0};
    getcwd(dir, 50);
    printf("%s\n", dir);
}

[root@localhost coding]# ./test
/root/coding

切换工作目录 chdir

函数声明:

int chdir(const char* path);

就像我们在shell中使用cd命令切换目录一样,在C程序中使用chdir函数来改变工作目录。

返回值:0-切换成功;非0-失败。

创建/删除目录 mkdir

创建目录:

int mkdir(const char* pathname, mode_t mode);

看描述,mode类似于目录权限,教程推荐默认0755,0表示为八进制,不可省略

例:

mkdir("/hello", 0644);

使用绝对路径名,在根目录“/”下创建了一个“hello”目录,权限644

[root@localhost coding]# ll / | grep hello
drw-r--r--.   2 root root    6 5月   3 16:58 hello

创建的目录的所有上级目录都必须存在,否则函数无效。

删除目录:

直接输入目录名即可,与rm -r指令等价。

int rmdir(const char *pathname);

只能删除空目录。对有内容的目录无效。

获取目录中的文件列表

像文件操作一样,打开、读取、关闭。

相关函数 open/read/close-dir

打开目录opendir

DIR* opendir(const char* pathname);

读取目录readdir

struct dirent* readdir(DIR* dirp);

关闭目录closedir

int closedir(DIR* dirp);
相关数据结构 dirent

struct dirent

每调用一次readdir函数会返回一个struct dirent的地址,存放了本次读取到的内容,它的原理与fgets函数读取文件相同。

是每次将一条目录信息读入

struct dirent
{
   long d_ino;	// inode number 索引节点号
   off_t d_off;	// offset to this dirent 在目录文件中的偏移
   unsigned short d_reclen;	// length of this d_name 文件名长
   unsigned char d_type;	// the type of d_name 文件类型
   char d_name [NAME_MAX+1];// file name文件名,最长255字符
};

只需要关注结构体的d_typed_name成员,其它的不必关心。

d_name文件名或目录名。

d_type描述了文件的类型,有多种取值,最重要的是8和4,8-常规文件(A regular file);4-目录(A directory),其它的暂时不关心。

例子
#include
#include
#include

int main(int argc, char* argv[])
{
	if(argc != 2)
    {
        printf("请再额外输入一个目录名\n");
        return -1;
    }
    
    DIR* pDir = 0;
    if((pDir = opendir(argv[1])) == 0)      return -1;

    struct dirent* dirInfo;
    while(1)
    {
    	if((dirInfo = readdir(pDir)) == 0)
    		break;
    printf("name=%s  type=%d\n", dirInfo->d_name, dirInfo->d_type);
    }
	closedir(pDir);
}
[root@localhost coding]# ./test /root/coding
name=.  type=4
name=..  type=4
name=read  type=8
name=write.c  type=8
name=xmlread  type=8
name=iofile.txt  type=8
name=read.c  type=8
name=write  type=8
name=XMLwrite.c  type=8
name=datasave.txt  type=8
name=XMLread.c  type=8
name=test.c  type=8
name=fordelete  type=4
name=test  type=8
name=test0.c  type=8
name=test0  type=8
name=xmlwrite  type=8
实例

实际需求是这样的,文件存放在某目录中,该目录下还会有多级子目录,程序员想要的是列出该目录及其子目录下全部的文件名。

#include
#include
#include


int ReadDir(const char* strPathName);

int main(int argc, char* argv[])
{
    if (argc != 2)
    {
        printf("请再额外输入一个目录名\n");
        return -1;
    }
    ReadDir(argv[1]);
}


int ReadDir(const char* strPathName)
{
    DIR* pDir = 0;
    char subPathName[256];
    memset(subPathName, 0, sizeof(subPathName));

    if ((pDir = opendir(strPathName)) == 0)
        return -1;

    struct dirent* pDirContent;
    while (1)
    {
        if ((pDirContent = readdir(pDir)) == 0)
            break;
		// 跳过以“.”开头的文件/目录
        if (strncmp(pDirContent->d_name, ".", 1) == 0)
            continue;
		// 如果是文件,将文件名与当前目录名一起输出
        if (pDirContent->d_type == 8)
            printf("name: %s/%s\n", strPathName, pDirContent->d_name);
		// 如果是目录,在当前目录名后拼接上目录名,递归调用。
        if (pDirContent->d_type == 4)
        {
            sprintf(subPathName, "%s/%s", strPathName, pDirContent->d_name);
            ReadDir(subPathName);
        }
    }
    closedir(pDir);
}

时间操作

#include

UNIX操作系统根据计算机产生的年代和应用采用1970年1月1日作为UNIX的纪元时间。将从1970年1月1日开始经过的秒数用一个整数存放,这种高效简洁的时间表示方法被称为“Unix时间纪元”。

time_t 别名 time

在C语言中,用time_t来表示时间数据类型。

typedef long time_t;

time函数

返回一个值:从1970年1月1日0时0分0秒到现在的秒数。

time_t time(time_t *t);

通过返回值接收和参数引用接收,效果相同。

如果只想通过返回值接收结果,可以在调用函数时传入0:

time(0);
#include
#include

int main()
{
        time_t t1;
        time_t t2 = time(&t1);
        printf("%d  %d\n", t1, t2);
}


[root@localhost coding]# ./test
1620044481  1620044481

tm 结构体 localtime mktime

比起time_t一个整型变量,更方便理解。

struct tm
{
  int tm_sec;     // 秒:取值区间为[0,59]
  int tm_min;     // 分:取值区间为[0,59]
  int tm_hour;    // 时:取值区间为[0,23]
  int tm_mday;    // 日期:一个月中的日期:取值区间为[1,31]
  int tm_mon;     // 月份:(从一月开始,0代表一月),取值区间为[0,11]
  int tm_year;    // 年份:其值等于实际年份减去1900
  int tm_wday;    // 星期:取值区间为[0,6],其中0代表星期天,1代表星期一,以此类推
  int tm_yday;    // 从每年的1月1日开始的天数:取值区间为[0,365],其中0代表1月1日,1代表1月2日,以此类推
  int tm_isdst;   // 夏令时标识符,该字段意义不大,我们不用夏令时。
};

注意:月份从0开始记,显示月份时要+1;

显示年份要+1900

localtime函数

time_t表示的时间转换为struct tm结构体。

struct tm* localtime(const time_t*);

mktime函数

mktime函数的功能与localtime函数相反。

mktime 函数用于把struct tm表示的时间转换为time_t表示的时间。

time_t mktime(struct tm *tm);
常用的标准输出格式
...
time_t t1 = time(0);
struct tm *sTime = localtime(&t1);


printf("%04u-%02u-%02u %02u:%02u:%02u\n", sTime->tm_year+1900, sTime->tm_mon+1, sTime->tm_mday, sTime->tm_hour, sTime->tm_min, sTime->tm_sec);

// 结果
[root@localhost coding]# ./test
2021-05-03 20:47:57

挂起程序

#include

在实际开发中,经常需要把程序挂起一段时间,可以使用sleep和usleep两个库函数。函数的声明如下:

unsigned int sleep(unsigned int seconds);
int usleep(useconds_t usec);

sleep函数的参数是秒,usleep函数的参数是微秒,1秒=1*10^6微秒。

返回值教程表示不用关心。

精确到微秒的计时器 time~val/zone

#include

sys/time.h 是Linux 系统的日期时间头文件,不能用于windows系统。

struct timeval

struct timeval
{
  long  tv_sec;  // 1970年1月1日到现在的秒。
  long  tv_usec; // 百万分之一秒,是当前秒的微妙,即从tv_sec开始经过了多少微秒。
};

struct timezone

struct timezone
{
  int tz_minuteswest;  // 和UTC(格林威治时间)差了多少分钟。
  int tz_dsttime;      // type of DST correction,修正参数据,忽略
};

gettimeofday

获得当前的秒和微秒的时间.

int gettimeofday(struct  timeval *tv, struct  timezone *tz )

函数执行成功后返回0,失败后返回-1。

第二个参数为时区信息,通常不关心,传入0。

一个计时器例子
#include
#include
#include

int main()
{
    struct timeval begin, end;

    gettimeofday(&begin, 0);
    printf("begin time: %d, %d, %d \n", time(0), begin.tv_sec, begin.tv_usec);

    sleep(2);
    usleep(100000);

    gettimeofday(&end, 0);
    printf("end time: %d, %d, %d \n", time(0), end.tv_sec, end.tv_usec);

    printf("计时时长为%d微秒 \n", (end.tv_sec - begin.tv_sec) * 1000000 + (end.tv_usec - end.tv_usec));
}
[root@localhost coding]# ./test
begin time: 1620047580, 1620047580, 660220 
end time: 1620047582, 1620047582, 763917 
计时时长为2000000微秒 

应用经验

在实际开发中,除了当前的时间,还经常需要一个偏移量的时间,例如获取十分钟之后的时间,方法是采用time函数得到一个整数后,再加上10*60秒,再用localtime函数转换为结构体。

预处理编译

从这部分的例子可以看出,基本上可以把宏放在代码中的任何地方。

预处理指令主要有以下三种:

1)包含文件:将源文件中以**#include**格式包含的文件复制到编译的源文件中,可以是头文件,也可以是其它的程序文件。

2)宏定义指令#define指令定义一个宏,#undef指令删除一个宏定义。

3)条件编译:根据**#ifdef#ifndef**后面的条件决定需要编译的代码。

#include略。

宏定义 #define #undef

在编译预处理过程时,对程序中所有出现的“宏名”,都用宏定义中的字符串去代换,这称为“宏替换”或“宏展开”。

在C语言中,宏分为有参数和无参数两种。

无参数的宏
#define 宏名 字符串

宏名:是一个标示符,必须符合C语言标示符的规定,一般以大写字母标识宏名。

字符串:可以是常数表达式格式串等。在前面使用的符号常量的定义就是一个无参数宏定义。

宏定义是宏名来表示一个字符串,在宏展开时又以该字符串取代宏名。这只是一种简单的代换,在编译预处理时不会对它进行语法检查,如有错误,只能在编译已被宏展开后的源程序时发现。

例子:

#define SAY_HELLO printf("hello,world! \n");
int main()
{
	SAY_HELLO
}

============================================
[root@localhost coding]# ./test
hello,world! 

宏定义允许嵌套,在宏定义的字符串中可以使用已经定义的宏名。在宏展开时由预处理程序层层替换。建议不要这么做,会把程序复杂化。

带参数的宏

#define命令定义宏时,还可以为宏设置参数。与函数中的参数类似,在宏定义中的参数为形参,在宏调用中的参数称为实参。

对带参数的宏,在调用中,不仅要宏展开,还要用实参去代换形参。

#define 宏名(形参表) 字符串

宏名和形参表之间不能有空格出现。

形参表内似乎能随便用空格。

例子:

#define MAX(x, y) ((x>y)?x:y)

int main()
{
	int a = 5;
	int b = 10;
	printf("%d \n", MAX(a,b));
}

================================
[root@localhost coding]# ./test
10 

带参的宏和函数相比,类似于内联函数与普通函数相比。带参的宏也是将源码替换到使用宏的位置,省去了函数调用的一系列操作。

#undef取消已定义的标识符。

条件编译 #ifdef #ifndef

#ifdef
#ifdef 标识符
  程序段 1
#else
  程序段 2
#endif

如果#ifdef后面的标识符已被定义过,则对“程序段1”进行编译;如果没有定义标识符,则编译“程序段2”。一般不使用#else及后面的“程序2”。

例子:

#define LINUX
 
int main()
{
  #ifdef LINUX
    printf("这是Linux操作系统。\n");
  #else
    printf("未知的操作系统。\n");
  #endif
}

========================================
[root@localhost coding]# ./test
这是Linux操作系统。
#ifndef
#ifndef 标识符
  程序段 1
#else
  程序段 2 
#endif

如果未定义标识符,则编译“程序段1”;否则编译“程序段2”

在实际开发中,程序员用#ifndef来防止头文件被重复包含。常与#define组合使用。例子:

#ifndef _PUBLIC_H
#define _PUBLIC_H 1
 
// 把字符串格式的时间转换为整数的时间,函数的声明如下:
int strtotime(const char *strtime,time_t *ti);
 
#endif

系统错误

教程中的所有使用库函数的例子中,都是通过函数的返回值判断调用是否成功。其实在C语言中,还有一个全局变量errno,存放了函数调用过程中产生的错误码。

为防止和正常的返回值混淆,库函数的调用一般并不直接返回错误码,而是将错误码(是一个整数值,不同的值代表不同的含义)存入一个名为 errno 的全局变量中,errno 不同数值所代表的错误消息定义在 文件中。如果库函数调用失败,可以通过读出 errno 的值来确定问题所在,推测程序出错的原因,这也是调试程序的一个重要方法。

配合 strerrorperror两个库函数,可以很方便地查看出错的详细信息。

strerror 中声明,用于获取错误码对应的消息描述。

perror 中声明,用于在屏幕上最近一次系统错误码消息描述,在实际开发中,我们写的程序运行于后台,在屏幕上显示错误信息没有意义。

strerror

char* strerror(int errno);

通过错误码errno查询错误,返回描述错误的字符串char*

例子:

#include
#include

int main()
{
	int i;
	for(i = 0; i < 10; ++i)
		printf("%d:%s \n", i, strerror(i));
}

========================================
[root@localhost coding]# ./test
0:Success 
1:Operation not permitted 
2:No such file or directory 
3:No such process 
4:Interrupted system call 
5:Input/output error 
6:No such device or address 
7:Argument list too long 
8:Exec format error 
9:Bad file descriptor

errno的细节

不是所有库函数都会设置errno的值

可以使用man指令查看函数说明,然后用?errno查询该函数是否有与errno相关的内容,做出判断。

errno不能用于判断库函数调用是否成功

在 C 语言中,如果库函数被正确地执行,那么 errno 的值不会被清零。也就是说,errno的值会保留为上次运行错误的库函数的置位值。

因此,在实际编程中,判断函数是否执行成功还得靠函数的返回值,只有在返回值是失败的情况下,才需要关注errno的值。

使用errno的例子

#include
#include
#include

int main()
{
	FILE* fp;
	if ((fp = fopen("/root/coding/hellothere", "r")) == 0)
		printf("打开文件失败。(%d:%s) \n", errno, strerror(errno));

	if (fp != 0)
		fclose(fp);
}

=======================================
[root@localhost coding]# ./test
打开文件失败。(2:No such file or directory) 

目录和文件操作扩展

access 判断权限

#include 
int access(const char *pathname, int mode);

判断文件pathname是否拥有mode权限。

pathname:文件名或目录名,可以是当前目录的文件或目录,也可以列出全路径。

mode:需要判断的存取权限。在头文件unistd.h中的预定义如下:

#define R_OK 4     // R_OK 只判断是否有读权限
#define W_OK 2    // W_OK 只判断是否有写权限
#define X_OK 1     // X_OK 判断是否有执行权限
#define F_OK 0     // F_OK 只判断是否存在

rwx 的值与 linux 系统一致。

当pathname满足mode的条件时候返回0不满足返回-1

在实际开发中,access函数主要用于判断文件或目录是否是存在。

stat 文件信息

#include 
#include 
#include 

在做练习时的教训:

对于用程序生成的二进制文件,可能获取到的st_mtime的结果为0,而不是该文件生成的日期。

stat 结构体

用于存放文件和目录的状态信息:

struct stat
{
  dev_t st_dev;   // device 文件的设备编号
  ino_t st_ino;   // inode 文件的i-node
  mode_t st_mode;   // protection 文件的类型和存取的权限
  nlink_t st_nlink;   // number of hard links 连到该文件的硬连接数目, 刚建立的文件值为1.
  uid_t st_uid;   // user ID of owner 文件所有者的用户识别码
  gid_t st_gid;   // group ID of owner 文件所有者的组识别码
  dev_t st_rdev;  // device type 若此文件为设备文件, 则为其设备编号
  off_t st_size;  // total size, in bytes 文件大小, 以字节计算
  unsigned long st_blksize;  // blocksize for filesystem I/O 文件系统的I/O 缓冲区大小.
  unsigned long st_blocks;  // number of blocks allocated 占用文件区块的个数, 每一区块大小为512 个字节.
  time_t st_atime;  // time of lastaccess 文件最近一次被存取或被执行的时间, 一般只有在用mknod、 utime、read、write 与tructate 时改变.
  time_t st_mtime;  // time of last modification 文件最后一次被修改的时间, 一般只有在用mknod、 utime 和write 时才会改变
  time_t st_ctime;  // time of last change i-node 最近一次被更改的时间, 此参数会在文件所有者、组、 权限被更改时更新
};

重点关注st_modest_sizest_mtime成员(权限、大小、最后被修改的时间)就可以了。st_mtime是一个整数表达的时间,需要程序员自己写代码转换格式。

st_mode成员的取值很多,或者使用如下两个宏来判断。

S_ISREG(st_mode)  // 是否为一般文件 
S_ISDIR(st_mode)  // 是否为目录
stat 库函数
int stat(const char *path, struct stat *buf);

获取path指定文件或目录的信息,并将信息保存到结构体buf中。

执行成功返回0,失败返回-1。

例子
#include
#include 
#include 

int main(int argc, char* argv[])
{
    if (argc != 2)
        printf("请输入一个文件/目录名。\n");
    if (access(argv[1], F_OK) != 0)
    {
        printf("文件/目录不存在。\n");
        return -1;
    }

    struct stat st;
    if (stat(argv[1], &st) != 0)
        return -1;

    if (S_ISREG(st.st_mode))
        printf("%s是个文件。\n", argv[1]);
    if (S_ISDIR(st.st_mode))
        printf("%s是个目录。\n", argv[1]);
}

===============================================
[root@localhost coding]# ./test /root/coding/fordelete
/root/coding/fordelete是个目录。
[root@localhost coding]# ./test /root/coding/read
/root/coding/read是个文件

utime 修改存取/更改时间

#include 

修改文件的存取时间和更改时间。

int utime(const char *filename, const struct utimbuf *times);

返回值:执行成功返回0,失败返回-1。

如果参数times为空指针(NULL), 则该文件的存取时间和更改时间全部会设为目前时间。

结构utimbuf 定义如下:

struct utimbuf
{
  time_t actime;
  time_t modtime;
};

rename 重命名(移动)

#include 

相当于linux操作系统的mv命令。

对程序员来说,在程序中极少重命名目录,但重命名文件是经常用到的功能。

int rename(const char *oldpath, const char *newpath);

oldpath:文件或目录的原名。newpath:文件或目录的新的名称。

返回值:0-成功,-1-失败。

实际上的行为的确和mv指令一样,可以将文件移动到其它目录中;

如果新的文件名与已经存在的文件重名,则会直接将其覆盖。

remove 删除

相当于操作系统的rm命令。

#include 
int remove(const char *pathname);

pathname:待删除的文件或目录名。

返回值:0-成功,-1-失败

gdb 调试程序

linux环境的调试,可以使用printf语句跟踪程序的运行,也可以使用调试工具。

用gcc编译源程序的时候,编译后的可执行文件不会包含源程序代码,如果您打算编译后的程序可以被调试,编译的时候要加-g的参数:

gcc -g -o test test.c

然后就能使用gdb指令调试了

gdb test
命令 命令缩写 命令说明
set args 设置主程序的参数。
break b 设置断点,b 20 表示在第20行设置断点,可以设置多个断点。
run r 开始运行程序, 程序运行到断点的位置会停下来,如果没有遇到断点,程序一直运行下去。
next n 执行当前行语句,如果该语句为函数调用,不会进入函数内部执行。
step s 执行当前行语句,如果该语句为函数调用,则进入函数执行其中的第一条语句。
print p 显示变量值,例如:p name表示显示变量name的值。
continue c 继续程序的运行,直到遇到下一个断点。
set var name=value 设置变量的值,假设程序有两个变量:int ii; char name[21];set var ii=10 把ii的值设置为10;set var name=“西施” 把name的值设置为"西施",注意,不是strcpy。
quit q 退出gdb环境。

set args 的例子

程序正常执行:
./book119 /oracle/c/book1.c /tmp/book1.c

gdb调试:
gdb book119
(gdb) set args /oracle/c/book1.c /tmp/book1.c

makefile

在软件的工程中的源文件是很多的,其按照类型、功能、模块分别放在若干个目录和文件中,哪些文件需要编译,那些文件需要后编译,那些文件需要重新编译,甚至进行更复杂的功能操作,这就有了我们的系统编译的工具。

在linux和unix中,有一个强大的实用程序,叫make,可以用它来管理多模块程序的编译和链接,直至生成可执行文件。

make程序需要一个编译规则说明文件,称为makefile,makefile文件中描述了整个软件工程的编译规则和各个文件之间的依赖关系。

makefile就像是一个shell脚本一样,其中可以执行操作系统的命令,它带来的好处就是我们能够实现“自动化编译”,一旦写好,只要一个make命令,整个软件功能就完全自动编译,提高了软件开发的效率。

make是一个命令工具,是一个解释makefile中指令的命令工具,一般来说大多数编译器都有这个命令,使用make可以使重新编译的次数达到最小化。

makefile的格式

all:test

test:test.c add.c
        gcc -o test test.c

clean:
        rm -f test
教程的解释
all:test

all:固定写法 test:需要编译目标程序的清单

test:test.c add.c

如果需要编译目标程序test,需要依赖源程序test.c和add.c,当二者任一个发生变化,就会重新执行下方指令。

        gcc -o test test.c

要执行的具体指令,必须由tab开头,可以有多行代码。

clean:

清除目标文件,清除的命令由第十行之后的脚本来执行。

        rm  -f  book1 book46

同上。

自己的理解
all:test noob 

test:test.c add.c
        gcc -o test test.c
        echo hello

clean:
        rm -f test

noob:
        echo hello,world!

执行结果:

[root@localhost coding]# make
echo hello,world!
hello,world!
--------------------------------------------
[root@localhost coding]# vi add.c
[root@localhost coding]# make
gcc -o test test.c
echo hello
hello
echo hello,world!
hello,world!

没有tab,占行首的alltestclean等可以视为一个指令域,可以通过make 指令域1 指令域2 ...的方式执行一或多个指令域内的命令,效果等价于直接在控制台输入指令。

all指令域后是默认执行的指令域。

对于test域,如果单独执行该域指令,会提示:(test域实际没有执行)

make: Nothing to be done for `all'.

在例子中,将其与noob域一起设为默认执行,只会输出noob域的执行结果。

只有在修改了test依赖的源程序,使其执行后,才会同时输出2个域的结果。

总结
all:[指令域1] [指令域2] [...]

指令域:[指令域的依赖文件1] [指令域的依赖文件2] [...]
	要执行的linux指令
	...

makefile 文件的变量

和linux变量很像。

变量的值都是字符串。

[root@localhost coding]# vi makefile
A=gcc
B=-o
C=echo

all:test

test:test.c add.c
        $(A) $(B) test test.c

alpha:
        $(C) $(A) $(B)
[root@localhost coding]# vi add.c 
[root@localhost coding]# make
gcc -o test test.c
[root@localhost coding]# make alpha
echo gcc -o
gcc -o

你可能感兴趣的:(c语言,linux)