UIUC CS241 讲义:众包系统编程书(1)https://developer.aliyun.com/article/1427159
printf
调用 write 还是 write 调用printf
?
printf
调用write
。printf
包括一个内部缓冲区,所以为了提高性能,printf
可能不会在每次调用printf
时都调用write
。printf
是一个 C 库函数。write
是一个系统调用,我们知道系统调用是昂贵的。另一方面,printf
使用一个更适合我们需求的缓冲区
如何打印出指针值?整数?字符串?
使用格式说明符“%p”表示指针,“%d”表示整数,“%s”表示字符串。所有格式说明符的完整列表在这里中找到
整数的例子:
int num1 = 10; printf("%d", num1); //prints num1
整数指针的例子:
int *ptr = (int *) malloc(sizeof(int)); *ptr = 10; printf("%p\n", ptr); //prints the address pointed to by the pointer printf("%p\n", &ptr); /*prints the address of pointer -- extremely useful when dealing with double pointers*/ printf("%d", *ptr); //prints the integer content of ptr return 0;
字符串的例子:
char *str = (char *) malloc(256 * sizeof(char)); strcpy(str, "Hello there!"); printf("%p\n", str); // print the address in the heap printf("%s", str); return 0;
如何将标准输出保存到文件?
最简单的方法:运行你的程序并使用 shell 重定向,例如
./program > output.txt #To read the contents of the file, cat output.txt
更复杂的方法:关闭(1),然后使用 open 重新打开文件描述符。参见cs-education.github.io/sys/#chapter/0/section/3/activity/0
指针和数组有什么区别?举一个你可以用其中一个做而另一个做不到的例子。
char ary[] = "Hello"; char *ptr = "Hello";
例子
数组名指向数组的第一个字节。ary
和ptr
都可以打印出来:
char ary[] = "Hello"; char *ptr = "Hello"; // Print out address and contents printf("%p : %s\n", ary, ary); printf("%p : %s\n", ptr, ptr);
数组是可变的,所以我们可以改变它的内容(但要小心不要写超出数组末尾的字节)。幸运的是,“World”不会比“Hello”更长
在这种情况下,char 指针ptr
指向一些只读内存(静态分配的字符串文字存储的地方),所以我们不能改变这些内容。
strcpy(ary, "World"); // OK strcpy(ptr, "World"); // NOT OK - Segmentation fault (crashes)
然而,与数组不同的是,我们可以将ptr
更改为指向另一块内存,
ptr = "World"; // OK! ptr = ary; // OK! ary = (..anything..) ; // WONT COMPILE // ary is doomed to always refer to the original array. printf("%p : %s\n", ptr, ptr); strcpy(ptr, "World"); // OK because now ptr is pointing to mutable memory (the array)
从中可以得出的结论是指针*可以指向任何类型的内存,而 C 数组[]只能指向堆栈上的内存。在更常见的情况下,指针将指向堆内存,这种情况下指针引用的内存是可以修改的。
sizeof()
返回字节数。所以使用上面的代码,ary
和ptr
的sizeof()
分别是多少?
sizeof(ary)
: ary
是一个数组。返回整个数组所需的字节数(5 个字符+零字节=6 个字节)sizeof(ptr)
: 与sizeof(char *)
相同。返回指针所需的字节数(例如 32 位或 64 位机器的 4 或 8)
sizeof
是一个特殊的运算符。实际上,它是编译程序之前编译器替换的东西,因为所有类型的大小在编译时是已知的。当你有sizeof(char*)
时,它会获取你的机器上指针的大小(64 位机器为 8 字节,32 位机器为 4 字节等)。当你尝试sizeof(char[])
时,编译器会查看并替换整个数组包含的字节数,因为数组的总大小在编译时是已知的。
char str1[] = "will be 11"; char* str2 = "will be 8"; sizeof(str1) //11 because it is an array sizeof(str2) //8 because it is a pointer
小心,使用 sizeof 获取字符串的长度!
以下代码中哪些是不正确的或正确的,为什么?
int* f1(int *p) { *p = 42; return p; } // This code is correct;
char* f2() { char p[] = "Hello"; return p; } // Incorrect!
解释:在堆栈上为包含 H,e,l,l,o 和一个空字节即(6)字节的正确大小创建了一个数组 p。这个数组存储在堆栈上,在我们从 f2 返回后就无效了。
char* f3() { char *p = "Hello"; return p; } // OK
解释:p 是一个指针。它保存了字符串常量的地址。字符串常量在 f3 返回后仍然保持不变和有效。
char* f4() { static char p[] = "Hello"; return p; } // OK
解释:数组是静态的,这意味着它存在于进程的整个生命周期(静态变量不在堆或栈上)。
如何查找 C 库调用和系统调用的信息?
使用 man 手册。请注意,man 手册分为几个部分。第二部分=系统调用。第三部分=C 库。网络:谷歌“man7 open” shell:man -S2 open 或 man -S3 printf
如何在堆上分配内存?
使用 malloc。还有 realloc 和 calloc。通常与 sizeof 一起使用。例如,足够的空间来容纳 10 个整数
int *space = malloc(sizeof(int) * 10);
这个字符串复制代码有什么问题?
void mystrcpy(char*dest, char* src) { // void means no return value while( *src ) { dest = src; src ++; dest++; } }
在上面的代码中,它只是改变了 dest 指针指向源字符串。而且 nuls 字节没有被复制。这是一个更好的版本 -
while( *src ) { *dest = *src; src ++; dest++; } *dest = *src;
请注意,通常还会看到以下类型的实现,其中包括在表达式测试中执行所有操作,包括复制 nul 字节。
while( (*dest++ = *src++ )) {};
如何编写一个 strdup 替代品?
// Use strlen+1 to find the zero byte... char* mystrdup(char*source) { char *p = (char *) malloc ( strlen(source)+1 ); strcpy(p,source); return p; }
如何在堆上取消分配内存?
使用 free!
int *n = (int *) malloc(sizeof(int)); *n = 10; //Do some work free(n);
什么是双重释放错误?如何避免?什么是悬空指针?如何避免?
双重释放错误是当您意外地尝试两次释放相同的分配时发生的。
int *p = malloc(sizeof(int)); free(p); *p = 123; // Oops! - Dangling pointer! Writing to memory we don't own anymore free(p); // Oops! - Double free!
修复首先是编写正确的程序!其次,一旦内存被释放,重置指针是良好的编程习惯。这确保了指针在没有程序崩溃的情况下不能被错误使用。
修复:
p = NULL; // Now you can't use this pointer by mistake
缓冲区溢出的一个例子是什么?
著名的例子:心脏出血(将一个 memcpy 复制到一个不足大小的缓冲区)。简单的例子:实现一个 strcpy 并忘记在确定所需内存大小时添加一个 strlen。
“typedef”是什么,你如何使用它?
声明类型的别名。通常与结构一起使用,以减少必须将“struct”写为类型的一部分的视觉混乱。
typedef float real; real gravity = 10; // Also typedef gives us an abstraction over the underlying type used. // For example in the future we only need to change this typedef if we // wanted our physics library to use doubles instead of floats. typedef struct link link_t; //With structs, include the keyword 'struct' as part of the original types
在这个课程中,我们经常使用 typedef 函数。例如,函数的 typedef 可以是这样的
typedef int (*comparator)(void*,void*); int greater_than(void* a, void* b){ return a > b; } comparator gt = greater_than;
这声明了一个接受两个void*
参数并返回整数的比较器函数类型。
哇,这是很多 C 的内容
别担心,还有更多要来的!
C 编程,第二部分:文本输入和输出
打印到流
如何将字符串、整数、字符打印到标准输出流中?
使用 printf
。第一个参数是格式字符串,其中包括要打印的数据的占位符。常见的格式说明符是 %s
将参数视为 C 字符串指针,一直打印到达到 NULL 字符为止;%d
将参数打印为整数;%p
将参数打印为内存地址。
下面显示了一个简单的示例:
char *name = ... ; int score = ...; printf("Hello %s, your result is %d\n", name, score); printf("Debug: The string and int are stored at: %p and %p\n", name, &score ); // name already is a char pointer and points to the start of the array. // We need "&" to get the address of the int variable
默认情况下,为了性能,printf
实际上并不会写任何东西(通过调用 write),直到它的缓冲区满或打印换行符。
我还可以如何打印字符串和单个字符?
使用 puts( name );
和 putchar( c )
,其中 name 是指向 C 字符串的指针,c 只是一个 char
如何将内容打印到其他文件流中?
使用 fprintf( _file_ , "Hello %s, score: %d", name, score);
其中 file 是预定义的 ‘stdout’ ‘stderr’ 或者是由 fopen
或 fdopen
返回的 FILE 指针
我可以使用文件描述符吗?
是的!只需使用 dprintf(int fd, char* format_string, ...);
只需记住流可能是缓冲的,所以您需要确保数据被写入文件描述符。
如何将数据打印到 C 字符串中?
使用 sprintf
或更好的 snprintf
。
char result[200]; int len = snprintf(result, sizeof(result), "%s:%d", name, score);
snprintf 返回写入的字符数,不包括终止字节。在上面的示例中,这将是最多 199 个。
如果我真的非常想要 printf
调用 write
而不换行怎么办?
使用 fflush( FILE* inp )
。文件的内容将被写入。如果我想要写入 “Hello World” 而不换行,我可以这样写。
int main(){ fprintf(stdout, "Hello World"); fflush(stdout); return 0; }
perror
有什么帮助?
假设您有一个函数调用刚刚失败了(因为您检查了 man 页面并且它是一个失败的返回代码)。perror(const char* message)
将把错误的英文版本打印到 stderr
int main(){ int ret = open("IDoNotExist.txt", O_RDONLY); if(ret < 0){ perror("Opening IDoNotExist:"); } //... return 0; }
解析输入
如何从字符串中解析数字?
使用 long int strtol(const char *nptr, char **endptr, int base);
或 long long int strtoll(const char *nptr, char **endptr, int base);
。
这些函数的作用是获取指向您的字符串 *nptr
和一个 base
(即二进制、八进制、十进制、十六进制等)以及一个可选的指针 endptr
,并返回解析的整数。
int main(){ const char *num = "1A2436"; char* endptr; long int parsed = strtol(num, &endptr, 16); return 0; }
但要小心!错误处理有点棘手,因为该函数不会返回错误代码。出错时,它将返回 0,您必须手动检查 errno,但这可能会导致麻烦。
int main(){ const char *zero = "0"; char* endptr; printf("Parsing number"); //printf sets errno long int parsed = strtol(num, &endptr, 16); if(parsed == 0){ perror("Error: "); //oops strtol actually worked! } return 0; }
如何使用 scanf
解析输入为参数?
使用 scanf
(或 fscanf
或 sscanf
)从默认输入流、任意文件流或 C 字符串中获取输入。检查返回值以查看解析了多少项是个好主意。scanf
函数需要有效的指针。将错误的指针值传入是一个常见的错误来源。例如,
int *data = (int *) malloc(sizeof(int)); char *line = "v 10"; char type; // Good practice: Check scanf parsed the line and read two values: int ok = 2 == sscanf(line, "%c %d", &type, &data); // pointer error
我们想要将字符值写入 c,将整数值写入 malloc’d 内存。然而我们传递的是数据指针的地址,而不是指针指向的内容!所以 sscanf
将会改变指针本身。也就是说,指针现在将指向地址 10,所以这段代码以后会失败,例如当调用 free(data)
时。
如何阻止 scanf 导致缓冲区溢出?
以下代码假设 scanf 不会读取超过 10 个字符(包括终止字节)到缓冲区中。
char buffer[10]; scanf("%s",buffer);
您可以包含一个可选的整数来指定多少个字符,不包括终止字节:
char buffer[10]; scanf("%9s", buffer); // reads upto 9 charactes from input (leave room for the 10th byte to be the terminating byte)
为什么 gets
是危险的?我应该用什么代替?
以下代码容易受到缓冲区溢出的影响。它假定或信任输入行不会超过 10 个字符,包括终止字节。
char buf[10]; gets(buf); // Remember the array name means the first byte of the array
gets
在 C99 标准中已被弃用,并且已从最新的 C 标准(C11)中删除。程序应该使用 fgets
或 getline
代替。
它们分别具有以下结构:
char *fgets (char *str, int num, FILE *stream); ssize_t getline(char **lineptr, size_t *n, FILE *stream);
下面是一种简单、安全的读取单行的方法。超过 9 个字符的行将被截断:
char buffer[10]; char *result = fgets(buffer, sizeof(buffer), stdin);
如果出现错误或者到达文件末尾,结果将为 NULL。请注意,与gets
不同,fgets
会将换行符复制到缓冲区中,您可能希望将其丢弃-
if (!result) { return; /* no data - don't read the buffer contents */} int i = strlen(buffer) - 1; if (buffer[i] == '\n') buffer[i] = '\0';
我如何使用getline
?
getline
的优点之一是它将自动(重新)分配足够大小的堆上的缓冲区。
// ssize_t getline(char **lineptr, size_t *n, FILE *stream); /* set buffer and size to 0; they will be changed by getline */ char *buffer = NULL; size_t size = 0; ssize_t chars = getline(&buffer, &size, stdin); // Discard newline character if it is present, if (chars > 0 && buffer[chars-1] == '\n') buffer[chars-1] = '\0'; // Read another line. // The existing buffer will be re-used, or, if necessary, // It will be `free`'d and a new larger buffer will `malloc`'d chars = getline(&buffer, &size, stdin); // Later... don't forget to free the buffer! free(buffer);
C 编程,第三部分:常见陷阱
C 程序员常犯哪些常见错误?
内存错误
字符串常量是常量
char array[] = "Hi!"; // array contains a mutable copy strcpy(array, "OK"); char *ptr = "Can't change me"; // ptr points to some immutable memory strcpy(ptr, "Will not work");
字符串文字是存储在程序的代码段中的字符数组,是不可变的。两个字符串文字可能共享内存中的相同空间。以下是一个例子:
char * str1 = "Brandon Chong is the best TA"; char * str2 = "Brandon Chong is the best TA";
由str1
和str2
指向的字符串实际上可能驻留在内存中的相同位置。
但是,char 数组包含了从代码段复制到堆栈或静态内存中的文字值。以下 char 数组不驻留在内存中的相同位置。
char arr1[] = "Brandon Chong didn't write this"; char arr2[] = "Brandon Chong didn't write this";
缓冲区溢出/下溢
#define N (10) int i = N, array[N]; for( ; i >= 0; i--) array[i] = i;
C 语言不检查指针是否有效。上面的例子写入了array[10]
,这超出了数组边界。这可能会导致内存损坏,因为该内存位置可能正在用于其他用途。实际上,这可能更难发现,因为溢出/下溢可能发生在库调用中。
gets(array); // Let's hope the input is shorter than my array!
返回指向自动变量的指针
int *f() { int result = 42; static int imok; return &imok; // OK - static variables are not on the stack return &result; // Not OK }
自动变量仅绑定到函数的堆栈内存,函数的生命周期结束后继续使用内存是错误的。
内存分配不足
struct User { char name[100]; }; typedef struct User user_t; user_t *user = (user_t *) malloc(sizeof(user));
在上面的例子中,我们需要为结构体分配足够的字节。相反,我们分配了足够的字节来容纳一个指针。一旦我们开始使用用户指针,就会破坏内存。正确的代码如下所示。
struct User { char name[100]; }; typedef struct User user_t; user_t * user = (user_t *) malloc(sizeof(user_t));
字符串需要strlen(s)+1
字节
每个字符串在最后一个字符后必须有一个空字节。存储字符串"Hi"
需要 3 个字节:[H] [i] [\0]
。
char *strdup(const char *input) { /* return a copy of 'input' */ char *copy; copy = malloc(sizeof(char*)); /* nope! this allocates space for a pointer, not a string */ copy = malloc(strlen(input)); /* Almost...but what about the null terminator? */ copy = malloc(strlen(input) + 1); /* That's right. */ strcpy(copy, input); /* strcpy will provide the null terminator */ return copy; }
使用未初始化的变量
int myfunction() { int x; int y = x + 2; ...
自动变量保存垃圾(内存中发生的任何位模式)。假设它总是初始化为零是错误的。
假设未初始化的内存将被清零
void myfunct() { char array[10]; char *p = malloc(10);
自动(临时变量)不会自动初始化为零。使用 malloc 进行堆分配不会自动初始化为零。
双重释放
char *p = malloc(10); free(p); // .. later ... free(p);
多次释放同一块内存是错误的。
悬空指针
char *p = malloc(10); strcpy(p, "Hello"); free(p); // .. later ... strcpy(p,"World");
不应使用指向释放内存的指针。一种防御性编程实践是在释放内存后立即将指针设置为 null。
将免费转换为以下片段是一个好主意,它会自动将释放的变量设置为 null:(vim - ultisnips)
snippet free "free(something)" b free(${1}); $1 = NULL; ${2} endsnippet
逻辑和程序流错误
忘记 break
int flag = 1; // Will print all three lines. switch(flag) { case 1: printf("I'm printed\n"); case 2: printf("Me too\n"); case 3: printf("Me three\n"); }
没有 break 的 case 语句将继续执行下一个 case 语句的代码。正确的代码如下所示。最后一个语句的 break 是不必要的,因为在最后一个语句之后没有更多的要执行的情况。但是,如果添加了更多的情况,可能会导致一些错误。
int flag = 1; // Will print only "I'm printed\n" switch(flag) { case 1: printf("I'm printed\n"); break; case 2: printf("Me too\n"); break; case 3: printf("Me three\n"); break; //unnecessary }
等号和相等
int answer = 3; // Will print out the answer. if (answer = 42) { printf("I've solved the answer! It's %d", answer);}
未声明或错误声明的函数
time_t start = time();
系统函数’time’实际上需要一个参数(一个指向可以接收 time_t 结构的一些内存的指针)。编译器没有捕获到这个错误,因为程序员没有通过包含time.h
提供有效的函数原型。
额外的分号
for(int i = 0; i < 5; i++) ; printf("I'm printed once"); while(x < 10); x++ ; // X is never incremented
然而,以下代码是完全可以的。
for(int i = 0; i < 5; i++){ printf("%d\n", i);;;;;;;;;;;;; }
这种代码是可以的,因为 C 语言使用分号(;)来分隔语句。如果分号之间没有语句,那么就没有要做的事情,编译器会继续执行下一条语句。
其他陷阱
预处理器
预处理器是什么?它是编译器在实际编译程序之前执行的操作。它是一个复制和粘贴命令。这意味着如果我做以下操作。
#define MAX_LENGTH 10 char buffer[MAX_LENGTH]
预处理后,它会变成这样。
char buffer[10]
C 预处理宏和副作用
#define min(a,b) ((a)<(b) ? (a) : (b)) int x = 4; if(min(x++, 100)) printf("%d is six", x);
宏是简单的文本替换,因此上面的例子会扩展为x++ < 100 ? x++ : 100
(为了清晰起见省略了括号)
C 预处理宏和优先级
#define min(a,b) a<b ? a : b int x = 99; int r = 10 + min(99, 100); // r is 100!
宏是简单的文本替换,因此上面的例子会扩展为10 + 99 < 100 ? 99 : 100
C 预处理逻辑陷阱
#define ARRAY_LENGTH(A) (sizeof((A)) / sizeof((A)[0])) int static_array[10]; // ARRAY_LENGTH(static_array) = 10 int* dynamic_array = malloc(10); // ARRAY_LENGTH(dynamic_array) = 2 or 1
宏有什么问题?如果我们有一个像第一个数组那样的静态数组,它就能工作,因为静态数组的 sizeof 返回数组占用的字节数,将其除以 sizeof(an_element)将给出条目的数量。但是,如果我们使用指向内存块的指针,取指针的 sizeof 并将其除以第一个条目的大小并不总是会给出数组的大小。
sizeof
有什么作用吗?
int a = 0; size_t size = sizeof(a++); printf("size: %lu, a: %d", size, a);
代码打印出什么?
size: 4, a: 0
因为 sizeof 实际上不是在运行时评估的。编译器为所有表达式分配类型并丢弃表达式的额外结果。
C 编程,第四部分:字符串和结构
字符串、结构和陷阱
那么什么是字符串?
在 C 中,���们使用空终止字符串,而不是长度前缀,出于历史原因。对于你平常的编程来说,这意味着你需要记住空字符!在 C 中,字符串被定义为一堆字节,直到你达到’\0’或空字节为止。
字符串的两个位置
每当你定义一个常量字符串(即形式为char* str = "constant"
的字符串)时,该字符串存储在数据或代码段中,这是只读的,这意味着任何尝试修改字符串都会导致段错误。
然而,如果有人malloc
空间,就可以更改该字符串为他们想要的任何内容。
内存管理不善
一个常见的陷阱是当你写下面的内容时
char* hello_string = malloc(14); ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ // hello_string ----> | g | a | r | b | a | g | e | g | a | r | b | a | g | e | ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ hello_string = "Hello Bhuvan!"; // (constant string in the text segment) // hello_string ----> [ "H" , "e" , "l" , "l" , "o" , " " , "B" , "h" , "u" , "v" , "a" , "n" , "!" , "\0" ] ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ // memory_leak -----> | g | a | r | b | a | g | e | g | a | r | b | a | g | e | ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ hello_string[9] = 't'; //segfault!!
我们做了什么?我们为 14 个字节分配了空间,重新分配了指针,成功地导致了段错误!记住跟踪你的指针在做什么。你可能想要做的是使用string.h
函数strcpy
。
strcpy(hello_string, "Hello Bhuvan!");
记住空字节!
忘记对字符串进行空终止会对字符串产生重大影响!边界检查很重要。前面在 wikibook 中提到的 heartbleed 漏洞部分是因为这个原因。
我在哪里可以找到所有这些函数的深入和全面的解释?
字符串信息/比较:strlen
strcmp
int strlen(const char *s)
返回字符串的长度,不包括空字节
int strcmp(const char *s1, const char *s2)
返回一个整数,确定字符串的词典顺序。如果 s1 在字典中出现在 s2 之前,则返回-1。如果两个字符串相等,则返回 0。否则返回 1。
对于大多数这些函数,它们期望字符串是可读的,而不是NULL
,但是当你传递NULL
时会出现未定义的行为。
字符串修改:strcpy
strcat
strdup
char *strcpy(char *dest, const char *src)
将src
的字符串复制到dest
。假设 dest 有足够的空间容纳 src
char *strcat(char *dest, const char *src)
将src
的字符串连接到目的地的末尾。此函数假定目的地末尾有足够的空间容纳src
,包括空字节
char *strdup(const char *dest)
返回字符串的malloc
副本。
字符串搜索:strchr
strstr
char *strchr(const char *haystack, int needle)
返回haystack
中needle
第一次出现的指针。如果找不到,则返回NULL
。
char *strstr(const char *haystack, const char *needle)
与上面相同,但这次是一个字符串!
字符串标记化:strtok
一个危险但有用的函数strtok
接受一个字符串并对其进行标记化。这意味着它将把字符串转换为单独的字符串。这个函数有很多规范,所以请阅读 man 页面,下面是一个人为的例子。
#include <stdio.h> #include <string.h> int main(){ char* upped = strdup("strtok,is,tricky,!!"); char* start = strtok(upped, ","); do{ printf("%s\n", start); }while((start = strtok(NULL, ","))); return 0; }
输出
strtok is tricky !!
当我像这样改变upped
时会发生什么?
char* upped = strdup("strtok,is,tricky,,,!!");
内存移动:memcpy
和memmove
为什么memcpy
和memmove
都在中?因为字符串本质上是带有空字节的原始内存!
void *memcpy(void *dest, const void *src, size_t n)
将从str
开始的n
个字节移动到dest
。小心 当内存区域重叠时会出现未定义的行为。这是一个经典的“在我的机器上工作”的例子,因为很多时候 valgrind 无法检测到它,因为在你的机器上它看起来是有效的。当自动评分器出现时,会失败。考虑更安全的版本。
void *memmove(void *dest, const void *src, size_t n)
做与上述相同的事情,但如果内存区域重叠,则保证所有字节都会正确复制过去。
那么struct
是什么?
从低级别来看,一个结构体只是一块连续的内存,仅此而已。就像数组一样,结构体有足够的空间来存储所有的成员。但与数组不同,它可以存储不同的类型。考虑上面声明的 contact 结构。
struct contact { char firstname[20]; char lastname[20]; unsigned int phone; }; struct contact bhuvan;
简短的插曲
/* a lot of times we will do the following typdef so we can just write contact contact1 */ typedef struct contact contact; contact bhuvan; /* You can also declare the struct like this to get it done in one statement */ typedef struct optional_name { ... } contact;
如果你在没有任何优化和重新排序的情况下编译代码,你可以期望每个变量的地址看起来像这样。
&bhuvan // 0x100 &bhuvan.firstname // 0x100 = 0x100+0x00 &bhuvan.lastname // 0x114 = 0x100+0x14 &bhuvan.phone // 0x128 = 0x100+0x28
因为你的编译器所做的就是说’嘿,保留这么多空间,我会去计算你想要写入的任何变量的偏移量’。
这些偏移量是什么意思?
偏移量是变量开始的地方。电话变量从第0x128
字节开始,持续 sizeof(int)字节,但并非总是如此。偏移量并不决定变量的结束位置。考虑在许多内核代码中看到的以下黑客行为。
typedef struct { int length; char c_str[0]; } string; const char* to_convert = "bhuvan"; int length = strlen(to_convert); // Let's convert to a c string string* bhuvan_name; bhuvan_name = malloc(sizeof(string) + length+1); /* Currently, our memory looks like this with junk in those black spaces ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ bhuvan_name = | | | | | | | | | | | | ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ */ bhuvan_name->length = length; /* This writes the following values to the first four bytes The rest is still garbage ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ bhuvan_name = | 0 | 0 | 0 | 6 | | | | | | | | ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ */ strcpy(bhuvan_name->c_str, to_convert); /* Now our string is filled in correctly at the end of the struct ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ____ bhuvan_name = | 0 | 0 | 0 | 6 | b | h | u | v | a | n | \0 | ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾‾ */ strcmp(bhuvan_name->c_str, "bhuvan") == 0 //The strings are equal!
但并不是所有的结构都是完美的
结构体可能需要一些叫做填充(教程)的东西。**我们不指望你在这门课程中对结构体进行打包,只是知道它存在。这是因为在早期(甚至现在)当你必须从内存中获取一个地址时,你必须以 32 位或 64 位块的方式进行。这也意味着你只能请求那些是它的倍数的地址。这意味着
struct picture{ int height; pixel** data; int width; char* enconding; } // You think picture looks like this height data width encoding ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ picture = | | | | | ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾
概念上可能看起来像这样
struct picture{ int height; char slop1[4]; pixel** data; int width; char slop2[4]; char* enconding; } height slop1 data width slop2 encoding ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ picture = | | | | | | | ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾
(这是在 64 位系统上)这并不总是这样,因为有时处理器支持不对齐访问。这是什么意思?嗯,有两种选择你可以设置一个属性
struct __attribute__((packed, aligned(4))) picture{ int height; pixel** data; int width; char* enconding; } // Will look like this height data width encoding ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ picture = | | | | | ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾
但现在每次我想要访问data
或encoding
,我都必须进行两次内存访问。你可以做的另一件事是重新排列结构,尽管这并不总是可能的
struct picture{ int height; int width; pixel** data; char* enconding; } // You think picture looks like this height width data encoding ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ picture = | | | | | ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾
C 编程,第五部分:调试
《C 程序调试指南》
这将是一个帮助您调试 C 程序的大型指南。您可以检查错误的不同级别,我们将逐个介绍。请随时添加您在调试 C 程序中发现有用的任何内容,包括但不限于,调试器的使用,识别常见错误类型,陷阱和有效的搜索技巧。
在代码中调试
清洁代码
使用辅助函数使您的代码模块化。如果有重复的任务(例如在 MP2 中获取连续块的指针),请将它们制作为辅助函数。确保每个函数都非常擅长做一件事,这样您就不必两次调试。
假设我们正在通过每次迭代找到最小元素来进行选择排序,如下所示,
void selection_sort(int *a, long len){ for(long i = len-1; i > 0; --i){ long max_index = i; for(long j = len-1; j >= 0; --j){ if(a[max_index] < a[j]){ max_index = j; } } int temp = a[i]; a[i] = a[max_index]; a[max_index] = temp; } }
许多人可以看到代码中的错误,但将上述方法重构为
long max_index(int *a, long start, long end); void swap(int *a, long idx1, long idx2); void selection_sort(int *a, long len);
而错误特别在一个函数中。
最后,我们不是一个关于重构/调试代码的课程–事实上,大多数系统代码都很糟糕,你不想读它。但是为了调试,长远来看,采用一些实践可能对你有好处。
断言!
使用断言来确保您的代码在某个特定点之前工作–并且重要的是,确保您以后不会破坏它。例如,如果您的数据结构是双向链表,您可以这样做,assert(node->size == node->next->prev->size)来断言下一个节点指向当前节点。您还可以检查指针是否指向预期的内存地址范围,而不是 null,->size 是合理的等等。NDEBUG 宏将禁用所有断言,因此在调试完成后不要忘记设置它。www.cplusplus.com/reference/cassert/assert/
使用 assert 的一个快速示例是,假设我正在使用 memcpy 编写代码
assert(!(src < dest+n && dest < src+n)); //Checks overlap memcpy(dest, src, n);
这个检查可以在编译时关闭,但会帮助您避免大量的调试麻烦!
printfs
当一切都失败时,疯狂地打印!您的每个函数都应该知道它要做什么(例如,find_min 最好找到最小的元素)。您希望测试每个函数是否正在做它设定的事情,并确切地查看代码在哪里出错。在竞态条件的情况下,tsan 可能有所帮助,但让每个线程在特定时间打印数据可能有助于您识别竞态条件。
Valgrind
(待办事项)
Tsan
ThreadSanitizer 是 Google 的一个工具,内置在 clang(和 gcc)中,可以帮助您检测代码中的竞态条件。有关该工具的更多信息,请参阅 Github 维基。
请注意,使用 tsan 会使您的代码变慢一些。
#include <pthread.h> #include <stdio.h> int Global; void *Thread1(void *x) { Global++; return NULL; } int main() { pthread_t t[2]; pthread_create(&t[0], NULL, Thread1, NULL); Global = 100; pthread_join(t[0], NULL); } // compile with gcc -fsanitize=thread -pie -fPIC -ltsan -g simple_race.c
我们可以看到变量 Global 存在竞态条件。主线程和使用 pthread_create 创建的线程将尝试同时更改值。但是,ThreadSantizer 能否捕捉到它呢?
$ ./a.out ================== WARNING: ThreadSanitizer: data race (pid=28888) Read of size 4 at 0x7f73ed91c078 by thread T1: #0 Thread1 /home/zmick2/simple_race.c:7 (exe+0x000000000a50) #1 :0 (libtsan.so.0+0x00000001b459) Previous write of size 4 at 0x7f73ed91c078 by main thread: #0 main /home/zmick2/simple_race.c:14 (exe+0x000000000ac8) Thread T1 (tid=28889, running) created by main thread at: #0 :0 (libtsan.so.0+0x00000001f6ab) #1 main /home/zmick2/simple_race.c:13 (exe+0x000000000ab8) SUMMARY: ThreadSanitizer: data race /home/zmick2/simple_race.c:7 Thread1 ================== ThreadSanitizer: reported 1 warnings
如果我们使用调试标志编译,那么它将给我们变量名。
GDB
介绍:www.cs.cmu.edu/~gilpin/tutorial/
以编程方式设置断点
在使用 GDB 调试复杂的 C 程序时,一个非常有用的技巧是在源代码中设置断点。
int main() { int val = 1; val = 42; asm("int $3"); // set a breakpoint here val = 7; }
$ gcc main.c -g -o main && ./main (gdb) r [...] Program received signal SIGTRAP, Trace/breakpoint trap. main () at main.c:6 6 val = 7; (gdb) p val $1 = 42
检查内存内容
www.delorie.com/gnu/docs/gdb/gdb_56.html
例如,
int main() { char bad_string[3] = {'C', 'a', 't'}; printf("%s", bad_string); }
$ gcc main.c -g -o main && ./main $ Cat ZVQ�� $
(gdb) l 1 #include <stdio.h> 2 int main() { 3 char bad_string[3] = {'C', 'a', 't'}; 4 printf("%s", bad_string); 5 } (gdb) b 4 Breakpoint 1 at 0x100000f57: file main.c, line 4. (gdb) r [...] Breakpoint 1, main () at main.c:4 4 printf("%s", bad_string); (gdb) x/16xb bad_string 0x7fff5fbff9cd: 0x63 0x61 0x74 0xe0 0xf9 0xbf 0x5f 0xff 0x7fff5fbff9d5: 0x7f 0x00 0x00 0xfd 0xb5 0x23 0x89 0xff (gdb)
在这里,通过使用带有参数16xb
的x
命令,我们可以看到从内存地址0x7fff5fbff9c
(bad_string
的值)开始,printf 实际上会看到以下字节序列作为字符串,因为我们提供了一个没有空终止符的格式不正确的字符串。
0x43 0x61 0x74 0xe0 0xf9 0xbf 0x5f 0xff 0x7f 0x00
C 编程,复习问题
主题
- C 字符串表示
- C 字符串作为指针
- char p[]vs char* p
- 简单的 C 字符串函数(strcmp,strcat,strcpy)
- sizeof char
- sizeof x vs x*
- 堆内存寿命
- 堆分配调用
- 解引用指针
- 取地址运算符
- 指针算术
- 字符串复制
- 字符串截断
- 双重释放错误
- 字符串字面值
- 打印格式。
- 内存越界错误
- 静态内存
- fileio POSIX v C 库
- C io fprintf 和 printf
- POSIX 文件 io(读|写|打开)
- stdout 的缓冲
问题/练习
- 以下打印出什么
int main(){ fprintf(stderr, "Hello "); fprintf(stdout, "It's a small "); fprintf(stderr, "World\n"); fprintf(stdout, "place\n"); return 0; }
- 以下两个声明之间有什么区别?其中一个的
sizeof
返回什么?
char str1[] = "bhuvan"; char *str2 = "another one";
- C 中的字符串是什么?
- 编写一个简单的
my_strcmp
。my_strcat
,my_strcpy
或my_strdup
呢?奖励:只通过字符串一次编写函数。 - 以下通常应该返回什么?
int *ptr; sizeof(ptr); sizeof(*ptr);
- 什么是
malloc
?它与calloc
有什么不同。一旦内存被malloc
,我如何使用realloc
? &
运算符是什么?*
呢?- 指针算术。假设以下地址。以下移位是什么?
char** ptr = malloc(10); //0x100 ptr[0] = malloc(20); //0x200 ptr[1] = malloc(20); //0x300
* `ptr + 2` * `ptr + 4` * `ptr[0] + 4` * `ptr[1] + 2000` * `*((int)(ptr + 1)) + 3`
- 我们如何防止双重释放错误?
- 打印字符串,
int
或char
的 printf 格式说明符是什么? - 以下代码有效吗?如果是,为什么?
output
位于哪里?
char *foo(int var){ static char output[20]; snprintf(output, 20, "%d", var); return output; }
- 编写一个接受字符串并打开该文件的函数,每次打印出文件的 40 个字节,但每隔一次打印都会颠倒字符串(尝试使用 POSIX API 实现)。
- POSIX 文件描述符模型和 C 的
FILE*
之间有哪些区别(即使用了哪些函数调用,哪个是缓冲的)?POSIX 内部使用 C 的FILE*
还是反之亦然?
二、进程
进程,第一部分:介绍
概述
进程是正在运行的程序(有点)。进程也只是计算机程序运行的一个实例。进程有很多可用的东西。在每个程序开始时,您会得到一个进程,但每个程序都可以创建更多的进程。事实上,您的操作系统只启动一个进程,所有其他进程都是从那个进程分叉出来的——在启动时都是在后台完成的。
在开始时
当您的 Linux 机器上的操作系统启动时,会创建一个名为init.d
的进程。该进程是一个特殊的进程,处理信号、中断和某些内核元素的持久性模块。每当您想要创建一个新进程时,都会调用fork
(将在后面的部分讨论)并使用另一个函数来加载另一个程序。
进程隔离
进程非常强大,但它们是隔离的!这意味着默认情况下,没有进程可以与另一个进程通信。这非常重要,因为如果您有一个庞大的系统(比如 EWS),那么您希望一些进程具有更高的特权(监控、管理),而您绝对不希望普通用户能够故意或者意外地通过修改进程来使整个系统崩溃。
如果我运行以下代码,
int secrets; //maybe defined in the kernel or else where secrets++; printf("%d\n", secrets);
在两个不同的终端上,正如您所猜测的,它们都会打印出 1 而不是 2。即使我们改变代码以执行一些非常巧妙的操作(除了直接读取内存),也没有办法改变另一个进程的状态(好吧,也许这个,但那就有点太深入了)。
进程内容
内存布局
当一个进程启动时,它会得到自己的地址空间。这意味着每个进程都会得到(对于内存
- 堆栈。堆栈是存储自动变量和函数调用返回地址的地方。每次声明一个新变量,程序都会将堆栈指针向下移动,以保留变量的空间。堆栈的这一部分是可写的,但不可执行。如果堆栈增长得太远(意味着它要么超出了预设的边界,要么与堆相交),您很可能会得到堆栈溢出,最终导致段错误或类似的错误。默认情况下,堆栈是静态分配的,这意味着只有一定数量的空间可以写入
- 堆。堆是一个不断扩大的内存区域。如果要分配一个大对象,它就会放在这里。堆从文本段的顶部开始向上增长(这意味着有时当您调用
malloc
时,它会要求操作系统将堆边界向上推)。这个区域也是可写的,但不可执行。如果系统受限或者地址用完了(在 32 位系统上更常见),就可能用完堆内存。 - 数据段。这包含了所有的全局变量。这一部分从文本段的末尾开始,大小是静态的,因为全局变量的数量在编译时就已知。这一部分是可写的,但不可执行,没有其他太花哨的东西。
- 文本段。这可以说是地址中最重要的部分。这是存储所有代码的地方。由于汇编编译成了 1 和 0,这就是 1 和 0 存储的地方。程序计数器在这个段中执行指令,并向下移动到下一个指令。重要的是要注意,这是代码中唯一可执行的部分。如果您尝试在运行时更改代码,很可能会导致段错误(虽然有办法绕过,但假设它会导致段错误)。
- 为什么它不从零开始?这超出了本课程的范围,但这是出于安全考虑。
文件描述符
正如小册子所示,操作系统跟踪文件描述符及其指向的内容。我们将在后面看到,文件描述符不一定指向实际文件,操作系统会为您跟踪它们。另外,请注意,在进程之间文件描述符可能会被重用,但在进程内部它们是唯一的。
文件描述符也有位置的概念。您可以完全从磁盘上读取文件,因为操作系统跟踪文件中的位置,并且该位置也属于您的进程。
安全/权限
进程功能/限制(奖励)
当您复习期末考试时,您可以回来看看进程也具有所有这些东西。第一次看时 - 它可能不太有意义。
进程 ID(PID)
为了跟踪所有这些进程,您的操作系统为每个进程分配一个数字,该进程称为 PID,即进程 ID。
进程还可以包含
- 映射
- 状态
- 文件描述符
- 权限
UIUC CS241 讲义:众包系统编程书(3)https://developer.aliyun.com/article/1427161