操作系统系列:快速了解C语言以及C编程

2023-12-16 07:18:40

操作系统系列:快速了解C语言以及C编程

本系列会写一些操作系统的知识,主要资料来源于国外大学的教材,供初学的人参考,欢迎大家讨论指正。

1 快速了解C语言

本系列内容所有代码使用C语言,因为要做很多系统调用,我们将在后面课程中介绍系统调用接口和操作系统的C函数调用接口,也会查看一些Unix内核源代码,而Unix的大多数版本主要是使用C语言编写的,再加上少量的汇编程序,而且大多数的Windows操作系统代码也是用C编写的,尽管它的代码并不公开,这个章节给出了一些针对C++程序员的简短的C语言教程。

如果你知道C++语言,那么C本质上是C++的一个子集,以下是二者之间的一些差异:

  1. C没有类的概念,或者说C和类之间没有任何关系,比如说它:
  • 没有构造函数
  • 没有析构函数
  • 没有继承的概念
  • 没有运算符重载
  • 没有模板

虽然C没有类的概念,但是它有结构体struct。结构体就像一个类一样,它可以有数据成员、但是它没有成员函数,并且相对于类来说,结构体的所有数据成员都是public。举例如下:

struct Student {
     char firstname[32];
     char lastname[32];
     int ID;
     double GPA;
};
.....
int main()
{
   struct Student Suzy;
   strcpy(Suzy.lastname,"Creamcheese");
   Suzy.ID = 234567;
   ...
}

Note: 声明结构体时,必须在名字前面加上struct。

  1. C的所有变量必须在函数或者块中的第一个可执行语句之前声明。下面这段代码在C中是不被允许的:
{
   struct Student Suzy;
   strcpy(Suzy.lastname,"Creamcheese");
   struct Student Monica; /* wrong */
   int i;   /* wrong */
   ...
}

如果用C来写这段代码,如下:

{
   struct Student Suzy;
   struct Student Monica;
   int i; 
   strcpy(Suzy.lastname,"Creamcheese");
   ...
}
  1. C的注释必须包含在 /* 和 */ 中。// 是C++风格的注释定界符,在C中是不允许使用的。

  2. 如果要在C编程中分配内存空间,必须使用系统调用void* malloc(int n),而不是像C++一样使用new。malloc的工作方式与new是类似的,它也是从堆中获取新的内存的地址并返回一个指针,参数n是分配的内存空间的字节数,在使用时,必须将malloc函数转换为正确的类型,也就是说void*要转换为合适的类型。举例如下:

char *cptr;
double *dblptr;
struct Student *stuptr;

cptr = (char *)malloc(100); 
							/* equiv to cptr = new char[100]; */
dblptr = (double *)malloc(sizeof(double) * n);
    						/* equiv to dblptr = new double[n]; */
stuptr = (struct Student *)malloc(sizeof(struct Student) * 47);
    						/* equiv to stuptr = new Student[47]; */
  1. C中的输入和输出方式与C++也是完全不同的。在C中不允许使用 << 和 >> ,并且 cin 和 cout 也是未定义的,它使用的输出函数是scanf()和printf()。这2个函数可以使用可变数量的参数,第一个参数使用格式字符串 (char* ) ,其余参数则是要在printf中写入或者在scanf中读取的变量。
    在printf函数的格式化字符串中,除了百分号字符%后面的字符外,其它所有字符都按照其本身的样子打印出来。这里%后面的字符表示应打印变量的值,%后面要紧跟着一个表示数据类型的字母,其中,d表示证书,c表示字符,s表示字符串,f表示浮点数或者双精度浮点数。在格式化字符串中的每一个%都应该有一个参数对应。
    以下是printf的一些示例:
printf("Hello World\n");  /* prints Hello World followed by a carriage
                             return, known in Unix as a newline char */ 
int x, y,;
char c = 'A';
char *s="Hello World";
x = 17;
printf("The value of x is %d", x);  /* prints The value of x is 17 */
y = 430;
printf("The sum of %d and %d is %d\n",x,y,x+y);
                           /* prints The sum of 17 and 430 is 447 */
printf("The ascii value of %c is %d\n",c,c);
                           /* prints The ascii value of A is 65 */
printf("%s\n",s);          /* prints Hello World */
函数scanf()是从键盘读取数据,与printf()工作方式相同。
char name[32];
int age;
printf("Please enter your name: ");
scanf("%s",name);
printf("Please enter your age: ");
scanf("%d",&age); 

请注意,除了scanf变量的字符串之外,还必须要传递变量名称的地址。

还有很多从标准输入(这里默认为是键盘)读取的函数:

char* gets(char* buf) -该函数从标准输入读入一行字符到缓冲区buf,直到出现换行符为止,这是一个不安全的函数,因为如果读入的字符数大于了缓冲区的空间,就会出现溢出。
int getchar() - 该函数从标准输入读取单个字符,再返回该字符。
int putchar(int c) - 该函数将字符c写到标准输出(默认指的是终端)
int puts(char* s) - 该函数将字符串s写到标准输出(默认指终端)

  1. C中的函数参数中不能有引用变量,要模拟引用调用,需要将变量的地址作为参数传递给函数,以下是个例子:
C program	                     				C++ equivalent
void fctn(int x, int *y)         				void fctn(int x, int& y)
{                               		 		{
    x = 17;                          	 			x = 17;
    *y = 46;                         	 			y = 46;
}                                	     		}
int main()                          			int main()
{                                				{
    int a = 12;                         			int a = 12;
    int b = 3;                           			int b = 3;
    fctn(a,&b);                        				fctn(a,b);
    printf("%d %d\n",a,b);      					cout << a << ' ' << b << endl;
    return 0;                           			return 0;
}                                       		}

以上两个示例打印了相同的输出:12,46

  1. C中包含I/O操作的头文件是<stdio.h>,而不是<iostream.h>

  2. C的源文件后缀是.c, 而不是.cpp,这是非常重要的区别,因为很多编译器(包括 Microsoft Visual Studio)会根据后缀不同而调用不同的编译器。

2 快速了解C编程

我们将使用Unix C编译器是gcc,即gnu c编译器。
首先,我们用最简单的形式使用gcc来编译一个C程序hello.c,在命令行中输入:

gcc hello.c

如果没有产生编译错误,会立即显示一个提示信息。可执行程序的名字是a.out,在命令行中输入下面的命令来运行这个程序。

./a.out

插入一些题外话:
Unix Shell(事实上所有的Unix进程都一样)都有一个内存区域称为环境,在其中定义了多个环境变量。Unix命令printenv会显示当前所有设置的环境变量。形式如下:

VARNAME=value

按照惯例,变量名全部大写,比如:

USER=ingallsr
HOME=/cs/ingallsr
TERM=dtterm

其中一个环境变量成为PATH,它由一个目录列表组成,由分号分隔,当您键入命令时,shell会在其中搜索可执行文件,要显示PATH的值,键入如下命令:

echo $PATH

输出可能类似于:

/usr/local/bin/:/usr/local/sbin/:/usr/bin/:/usr/local/X11R6/bin/

这与在windows上输入指令不同,在windows系统它的指令是:
>echo &PATH

当键入 a.out 等命令时,shell 必须找到该可执行文件的位置。 它先搜索路径中的第一个目录 (/usr/local/bin),然后搜索第二个目录 (/usr/local/sbin),然后搜索第三个目录 (/usr/bin),依此类推,直到找到该目录的可执行文件 名称,然后执行它。 如果路径中的任何目录中都没有名为a.out的可执行程序,shell将显示command not find。 如果路径中的多个目录中有名为 a.out 的可执行文件,则它将仅执行找到的第一个文件。

命令 ./a.out 中的第一个点指的是当前目录。 这是在告诉 shell 不要去搜索路径中的目录了,只需要在当前工作目录中搜索就行了。
对于这个 . 是否应该出现在PATH的目录列表中存在一些争议,如果是,它到底是指的哪里呢?其中一派说,. 应该是指示路径中的第一个条目,如果是这样的话,那么你只需输入 a.out就可以了,它会执行你刚刚编译的程序。但另一派的观点则认为 . 应该指示的是文件列表的最后一个路径,那么在这种情况下,如果输入 a.out,它将执行刚刚通过编译创建的文件,除非路径列表中的某个目录中恰好有另一个名为 a.out 的可执行文件,这时另一个a.out将会被执行。还有一派则说,根本就不需要把这个 . 加到路径列表中,系统管理员会为你设置一个默认路径,而且通常来说,这个 . 不在默认路径中。

反对在路径中添加点的理由是,这是一个潜在的安全问题。 如果有人入侵了您的计算机,他们可以将一个会执行恶意操作的名为 ls 的可执行文件放入您的主目录中,下次您登录并输入 ls(这是最常用的命令)时,它将执行恶意版本,而不是执行系统 的ls。

在主目录中应该有一个名为 .bashrc 的隐藏文件,(在 Unix 中,隐藏文件以点作为第一个字符。如果您希望 ls 显示隐藏文件,请使用 -a 选项 (ls -a))。该文件包含每当 bash 启动时运行的语句。 如果你想把 . 放在路径末尾,将以下行添加到 .bashrc。

export PATH=$PATH:.

shell命令export用于设置要导出到任何它所启动的程序的环境,该命令表示将 :. 连接到当前路径的末尾,再将 PATH 的值设置为其当前值。
将 . 加入到路径的开头,那么这样设置:

export PATH=.:$PATH

题外话结束,回归正题

C 编译器实际上有数百个选项,可以通过输入 man gcc 来了解所有这些内容,只有其中的少数内容对本课程是重要的。

-o filenema 将输出可执行文件名为filename,而不是a.out
-Wall 和错误消息一样显示所有告警,在提交代码之前,要尝试消除产生告警的所有条件,比如未使用的变量。
-g 产生调试信息。Unix上有个gdb调试器(对于图形用户接口是xxgdb),如果你想使用这个调试器,那么编译时需要使用这个标志。

这里简短的介绍一下gdb。

使用GNU调试器GDB

  • 使用 -g 选项编译程序(gcc -g file.c

  • 可执行文件和源代码应位于同一目录中。 启动调试器指令为:
    gdb executable,例如 gdb a.out

  • 大多数命令都可以通过其首字母来调用; 您无需输入整个单词。

  • 在运行程序之前,使用break命令设置一个或多个断点,可以在行号或函数的开头设置。 例如

    b 136 		是指在第 136 行处中断,
    或者 		
    b fctnOne 	是指在 fctnOne 的开头处中断 
    

这里有一些更有用的命令:

run
开始运行程序,将运行到第一个断点,可以将参数传递给运行命令,它会将参数传递给您的程序。 例如: r arg1 arg2
print
显示变量或表达式的值
step
一次一行地执行程序,进入函数。
next
一次一行地执行程序,单步执行函数
continue
运行到下一个断点
list
显示源代码
quit
终止调试器
help
获取更多信息

任何大型程序都会有多个源文件,并且要传递多个源文件作为gcc的参数,它们合并在一起产生一个可执行程序,在所有源文件中有且只有一个main函数,它是入口函数。
比如,

gcc FileOne.c FileTwo.c FileThree.c

那么将会生成一个可执行程序a.out

gcc -o hello -g FileOne.c FileTwo.c FileThree.c

将会生成一个名为hello的可执行程序,并且可以使用gdb来调试它。

文章来源:https://blog.csdn.net/wss_xt/article/details/134986272
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。