操作系统系列:快速了解C语言以及C编程
本系列会写一些操作系统的知识,主要资料来源于国外大学的教材,供初学的人参考,欢迎大家讨论指正。
1 快速了解C语言
本系列内容所有代码使用C语言,因为要做很多系统调用,我们将在后面课程中介绍系统调用接口和操作系统的C函数调用接口,也会查看一些Unix内核源代码,而Unix的大多数版本主要是使用C语言编写的,再加上少量的汇编程序,而且大多数的Windows操作系统代码也是用C编写的,尽管它的代码并不公开,这个章节给出了一些针对C++程序员的简短的C语言教程。
如果你知道C++语言,那么C本质上是C++的一个子集,以下是二者之间的一些差异:
- 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。
- 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");
...
}
-
C的注释必须包含在 /* 和 */ 中。// 是C++风格的注释定界符,在C中是不允许使用的。
-
如果要在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]; */
- 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写到标准输出(默认指终端)
- 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
-
C中包含I/O操作的头文件是<stdio.h>,而不是<iostream.h>
-
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
-Wal
l 和错误消息一样显示所有告警,在提交代码之前,要尝试消除产生告警的所有条件,比如未使用的变量。
-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
显示变量或表达式的值
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来调试它。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!