提交 bd95e22e 编写于 作者: W wizardforcel

fix conflict

......@@ -4,15 +4,15 @@
C 程序员常犯的错误是什么?
## 记忆错误
## 内存错误
## 字符串常量是常量
### 字符串常量是常量
```c
char array[] = "Hi!"; // array contains a mutable copy
char array[] = "Hi!"; // 数组是一个可变的复制
strcpy(array, "OK");
char *ptr = "Can't change me"; // ptr points to some immutable memory
char *ptr = "Can't change me"; // ptr 指向不可变的内存
strcpy(ptr, "Will not work");
```
......@@ -21,6 +21,7 @@ strcpy(ptr, "Will not work");
```c
char * str1 = "Brandon Chong is the best TA";
char * str2 = "Brandon Chong is the best TA";
str1 == str2;// true
```
`str1``str2`指向的字符串实际上可能位于内存中的相同位置。
......@@ -30,9 +31,11 @@ char * str2 = "Brandon Chong is the best TA";
```c
char arr1[] = "Brandon Chong didn't write this";
char arr2[] = "Brandon Chong didn't write this";
arr1 == arr2;// false
&arr1[0] == &arr2[0];// false
```
## 缓冲区溢出/下溢
### 缓冲区溢出/下溢
```c
#define N (10)
......@@ -40,26 +43,33 @@ int i = N, array[N];
for( ; i >= 0; i--) array[i] = i;
```
C 不检查指针是否有效。上面的例子写入`array[10]`,它位于数组边界之外。这可能会导致内存损坏,因为该内存位置可能正在用于其他内容。在实践中,这可能更难以发现,因为溢出/下溢可能发生在调用中,例如
C 不检查指针是否有效。上面的例子写入`array[10]`,它位于数组边界之外。这可能会导致内存损坏,因为该内存位置可能正在用于其他内容。在实践中,这可能更难以发现,因为溢出/下溢可能发生在使用不安全的库调用中或者设置错误的大小限制在安全库的调用中,例如
```c
gets(array); // Let's hope the input is shorter than my array!
gets(array); // 我们希望输入比我的array短!(永远不要使用gets)
fgets(array, 4096, stdin); //Whoops
```
## 返回指向自动变量的指针
### 返回指向自动变量的指针
```c
int *f() {
int result = 42;
static int imok;
return &imok; // OK - static variables are not on the stack
return &result; // Not OK
int *p;
{
int x = result;
p = &x;
}
//imok = *p; // Not OK: x不在定义域内
//return &result; // Not OK: result将不在定义域内在函数返回之后
return &imok; // OK - 静态成员不在栈区
}
```
自动变量仅在函数的生命周期内绑定到栈内存。函数返回后,继续使用内存是错误的
自动变量仅在函数的生命周期内绑定到栈内存。函数返回后,储存在栈区的数据将变为`undefined`,静态变量储存在数据区,当函数返回后依然可以被使用
## 内存分配不足
### sizeof(type *) 对比 sizeof(type)
```c
struct User {
......@@ -81,22 +91,22 @@ typedef struct User user_t;
user_t * user = (user_t *) malloc(sizeof(user_t));
```
#### 字符串需要`strlen(s)+1`个字节
### 字符串需要`strlen(s)+1`个字节
每个字符串在最后一个字符后必须有一个空字节。要存储字符串`"Hi"`,需要 3 个字节:`[H] [i] [\0]`
```c
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(sizeof(char*)); /* nope! 分配了一个指针大小的空间,不是一个字符串 */
copy = malloc(strlen(input)); /* Almost...null 终止符被遗忘了 */
copy = malloc(strlen(input) + 1); /* That's right. */
strcpy(copy, input); /* strcpy will provide the null terminator */
strcpy(copy, input); /* strcpy 将会提供null终止符 */
return copy;
}
```
## 使用未初始化的变量
### 使用未初始化的变量
```c
int myfunction() {
......@@ -107,17 +117,18 @@ int myfunction() {
自动变量保存垃圾(无论位模式发生在内存中)。假设它将始终初始化为零是错误的。
## 假设未初始化的内存将被清零
### 初始化内存错误
```c
void myfunct() {
char array[10];
char *p = malloc(10);
printf("%s %s\n", array, p);
```
自动(临时变量)不会自动初始化为零。使用 malloc 的堆分配不会自动初始化为零
自动(栈内)变量和用`malloc`分配的堆内存不会自动初始化为零。这个函数的结果是未定义行为
## 双免费
### 双重释放
```c
char *p = malloc(10);
......@@ -128,7 +139,7 @@ void myfunct() {
将同一块内存释放两次是错误的。
## 晃来晃去的指针
### 游荡的指针
```c
char *p = malloc(10);
......@@ -138,24 +149,45 @@ void myfunct() {
strcpy(p,"World");
```
不应使用释放内存的指针。防御性编程实践是在释放内存后立即将指针设置为 null。
访问被释放的内存是未定义行为。防御性编程实践是在释放内存后立即将指针设置为 null,
下面的宏可以完成这种操作。
最好将 free 释放到以下片段中,该片段在以下情况下自动将释放的变量设置为 null:(vim - ultisnips)
```c
#define safer_free(p) {free(p); (p) = NULL;}
```
```source-viml
snippet free "free(something)" b
free(${1});
$1 = NULL;
${2}
endsnippet
### 忘记了复制`getline`缓存
```c
#include <stdio.h>
int main(void){
char *line = NULL;
size_t linecap = 0;
char *strings[3];
// assume stdin contains "1\n2\n\3\n"
for (size_t i = 0; i < 3; ++i)
strings[i] = getline(&line, &linecap, stdin) >= 0 ? line : "";
// this prints out "3\n3\n\3" instead of "3\n\2\n1\n"
for (size_t i = 3; i--;) // i=2,1,0
printf("%s", strings[i]);
}
```
getline重复是一个缓存,所有的指针在strings指向同一块内存。我们可以通过对strings[i]进行深复制来修复它。
```c
strings[i] = getline(&line, &linecap, stdin) >= 0 ? strdup(line) : "";
```
## 逻辑和程序流程错误
## 忘了休息
### 忘了break在使用case之后
```c
int flag = 1; // Will print all three lines.
int flag = 1; // 会打印出所有行
switch(flag) {
case 1: printf("I'm printed\n");
case 2: printf("Me too\n");
......@@ -180,26 +212,39 @@ switch(flag) {
}
```
## 平等与平
### 赋值和相
```c
int answer = 3; // Will print out the answer.
if (answer = 42) { printf("I've solved the answer! It's %d", answer);}
```
## 未声明或不正确的原型功能
编译器会警告你这个错误,如果你想要执行一个赋值,添加一个额外的括号去消除这个警告。
```c
time_t start = time();
ssize_t x;
if ( (x = read(somefd, somebuf, somenum)) ){
// do something
}
```
### 未声明或不正确的原型功能
```c
#include <stdio.h>
int main(void){
int start = time();
printf("%d\n", start);
}
```
系统函数'time'实际上是一个参数(指向一些可以接收 time_t 结构的内存的指针)。编译器没有捕获此错误,因为程序员没有通过包含`time.h`来提供有效的函数原型
## 额外的分号
### 额外的分号
```c
for(int i = 0; i < 5; i++) ; printf("I'm printed once");
while(x < 10); x++ ; // X is never incremented
while(x < 10); x++ ; // X 永远不会增加
```
但是,以下代码完全可以。
......@@ -214,9 +259,9 @@ for(int i = 0; i < 5; i++){
## 其他陷阱
## 预处理器
### 预处理器
什么是预处理器?这是在实际编译程序之前编译器执**的操作。它是一个复制和粘贴命令。这意味着如果我执行以下操作。**
什么是预处理器?这是在实际编译程序之前编译器执的操作。它是一个**复制和粘贴**命令。这意味着如果我执行以下操作。
```c
#define MAX_LENGTH 10
......@@ -229,7 +274,7 @@ char buffer[MAX_LENGTH]
char buffer[10]
```
## C 预处理器宏和副作用
### C 预处理器宏和副作用
```c
#define min(a,b) ((a)<(b) ? (a) : (b))
......@@ -239,7 +284,7 @@ if(min(x++, 100)) printf("%d is six", x);
宏是简单的文本替换,因此上面的示例扩展为`x++ &lt; 100 ? x++ : 100`(为清楚起见省略了括号)
## C 预处理器宏和优先级
### C 预处理器宏和优先级
```c
#define min(a,b) a<b ? a : b
......@@ -249,7 +294,7 @@ int r = 10 + min(99, 100); // r is 100!
宏是简单的文本替换,因此上面的示例扩展为`10 + 99 &lt; 100 ? 99 : 100`
## C 预处理器逻辑问题
### C 预处理器逻辑问题
```c
#define ARRAY_LENGTH(A) (sizeof((A)) / sizeof((A)[0]))
......@@ -259,7 +304,7 @@ int* dynamic_array = malloc(10); // ARRAY_LENGTH(dynamic_array) = 2 or 1
宏有什么问题?好吧,如果我们有一个像第一个数组那样的静态数组,那么它是有效的,因为静态数组的 sizeof 返回数组占用的字节数,并将它除以 sizeof(an_element)将得到条目数。但是如果我们使用指向一块内存的指针,那么获取指针的大小并将其除以第一个条目的大小并不总能给出数组的大小。
## `sizeof`有什么用吗?
### `sizeof`的副作用
```c
int a = 0;
......
# C 编程,第 4 部分:字符串和结构
# C 编程,第 4 部分:字符串和结构
> 原文:<https://github.com/angrave/SystemProgramming/wiki/C-Programming%2C-Part-4%3A-Strings-and-Structs>
......@@ -10,13 +10,13 @@
在 C 中,由于历史原因,我们有 [Null Terminated](https://en.wikipedia.org/wiki/Null-terminated_string) 字符串而不是 [Length Prefixed](https://en.wikipedia.org/wiki/String_(computer_science)#Length-prefixed) 。对于平均日常编程而言,这意味着您需要记住空字符! C 中的字符串被定义为一串字节,直到您达到'\ 0'或空字节。
## 两个字符串的地方
### 两个不同地方的字符串
每当你定义一个常量字符串(即`char* str = "constant"`形式的那个)时,该字符串存储在数据或 _ 代码 _ 段中**只读**含义任何修改字符串的尝试都会导致段错误。
每当你定义一个常量字符串(即`char* str = "constant"`形式的那个)时,该字符串存储在数据或代码 段中,**只读**意味着任何修改字符串的尝试都会导致段错误。
如果有一个`malloc`的空间,可以将该字符串更改为他们想要的任何内容。
如果一个字符串在`malloc`的空间,可以将该字符串更改为他们想要的任何内容。
## 记忆管理不善
### 内穿管理不善
一个常见的问题是当你写下面的内容时
......@@ -31,24 +31,24 @@ hello_string = "Hello Bhuvan!";
___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___
// memory_leak -----> | g | a | r | b | a | g | e | g | a | r | b | a | g | e |
‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾
hello_string[9] = 't'; //segfault!!
hello_string[9] = 't'; //segfault!!分段错误
```
我们做了什么?我们为 14 个字节分配了空间,重新分配指针并成功进行了 segfaulted!记得要记录你的指针在做什么。您可能想要做的是使用`string.h`功能`strcpy`
我们做了什么?我们为 14 个字节分配了空间,重新分配指针并成功实现了分段错误!记得要记录你的指针在做什么。您可能想要做的是使用`string.h`功能`strcpy`
```c
strcpy(hello_string, "Hello Bhuvan!");
```
## 记住 NULL 字节!
### 记住 NULL 字节!
忘记 NULL 终止字符串对字符串有很大的影响!界限检查很重要。之前在 wikibook 中提到的心脏病是部分原因
忘记 NULL 终止字符串对字符串有很大的影响!界限检查很重要,它是之前在 wikibook 中提到的重要bug的一部分
## 我在哪里可以找到所有这些功能的深入和分配 - 综合解释?
### 我在哪里可以找到所有这些功能的深入和分配 - 综合解释?
[就在这里!](https://linux.die.net/man/3/string)
## 字符串信息/比较:`strlen` `strcmp`
### 字符串信息/比较:`strlen` `strcmp`
`int strlen(const char *s)`返回不包括空字节的字符串的长度
......@@ -56,7 +56,7 @@ strcpy(hello_string, "Hello Bhuvan!");
对于大多数这些函数,他们希望字符串是可读的而不是 NULL,但是当你将它们传递给 NULL 时会有未定义的行为。
## 字符串更改:`strcpy` `strcat` `strdup`
### 字符串更改:`strcpy` `strcat` `strdup`
`char *strcpy(char *dest, const char *src)``src`处的字符串复制到`dest`**假设 dest 有足够的空间用于 src**
......@@ -64,15 +64,15 @@ strcpy(hello_string, "Hello Bhuvan!");
`char *strdup(const char *dest)`返回字符串的`malloc`编辑副本。
## 字符串搜索:`strchr` `strstr`
### 字符串搜索:`strchr` `strstr`
`char *strchr(const char *haystack, int needle)`返回指向`haystack`中第一次出现`needle`的指针。如果没有找到,则返回`NULL`
`char *strstr(const char *haystack, const char *needle)`与上面相同,但这次是一个字符串!
## 字符串标记:`strtok`
### 字符串切分:`strtok`
一个危险但有用的函数 strtok 需要一个字符串并将其标记化。这意味着它会将字符串转换为单独的字符串。这个函数有很多规格,所以请阅读手册,下面是一个人为的例子。
一个危险但有用的函数 strtok 需要一个字符串并将其切分。这意味着它会将字符串转换为单独的字符串。这个函数有很多规格,所以请阅读手册,下面是一个人为的例子。
```c
#include <stdio.h>
......@@ -103,19 +103,19 @@ tricky
char* upped = strdup("strtok,is,tricky,,,!!");
```
## 记忆运动:`memcpy`和`memmove`
### 内存移动:`memcpy`和`memmove`
为什么`&lt;string.h&gt;``memmove`都在`&lt;string.h&gt;`中?因为字符串本质上是原始内存,在它们的末尾有一个空字节!
为什么`memcpy``memmove`都在`<string.h>`中?因为字符串本质上是原始内存,在它们的末尾有一个空字节!
`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`?
## 那么什么是`struct`结构体
![Struct Example](img/921a3e0534d1220fea61c43535dadd2c.jpg)
在低层次上,结构只是一块连续的内存,仅此而已。就像数组一样,struct 有足够的空间来保留其所有成员。但与数组不同,它可以存储不同的类型。考虑上面声明的 contact 结构
在低层次上,结构只是一块连续的内存,仅此而已。就像数组一样,struct 有足够的空间来保留其所有成员。但与数组不同,它可以存储不同的类型。考虑上面声明的 contact 结构
```c
struct contact {
......@@ -127,7 +127,7 @@ struct contact {
struct contact bhuvan;
```
**暂且不谈**
**简短的写法**
```c
/* a lot of times we will do the following typdef
......@@ -154,9 +154,9 @@ typedef struct optional_name {
因为你所有的编译器都说'嘿保留这么多空间,我将去计算你想写的任何变量的偏移'。
## 这些补偿是什么意思?
### 这些偏移是什么意思?
偏移量是变量开始的位置。电话变量从`0x128`个字节开始,并继续为 sizeof(int)字节,但并非总是如此。 **偏移不会确定**变量的结束位置。考虑一下你在很多内核代码中看到的以下 hack。
偏移量是变量开始的位置。联系变量从`0x128`个字节开始,并继续为 sizeof(int)字节,但并非总是如此。 **偏移不会确定**变量的结束位置。考虑一下你在很多内核代码中看到的以下 hack。
```c
typedef struct {
......@@ -198,9 +198,9 @@ Now our string is filled in correctly at the end of the struct
strcmp(bhuvan_name->c_str, "bhuvan") == 0 //The strings are equal!
```
## 但并非所有结构都是完美的
### 但并非所有结构都是完美的
结构可能需要一些称为[填充](http://www.catb.org/esr/structure-packing/)(教程)的东西。 **我们不希望你在这个课程中打包结构,只知道它在那里这是因为在早期(甚至现在)当你需要从内存中的地址时你必须在 32 位或 64 位块中进行。这也意味着您只能请求多个地址的地址。意思是
结构体可能需要一些称为[填充](http://www.catb.org/esr/structure-packing/)(教程)的东西。 **我们不希望你在这个课程中打包结构,只知道它在那里这是因为在早期(甚至现在)当你需要从内存中的地址时你必须在 32 位或 64 位块中进行。这也意味着您只能请求多次来获得相应的地址。意思是
```c
struct picture{
......
......@@ -4,11 +4,11 @@
## Hitchhiker 调试 C 程序指南
这将成为帮助您调试 C 程序的重要指南。有不同的级别可以检查错误,我们将通过大多数错误。随意添加您在调试 C 程序时发现有用的任何内容,包括但不限于调试器使用,识别常见错误类型,陷阱和有效的 Google 搜索提示。
这将成为帮助您调试 C 程序的重要指南。有不同的层次的方法可以检查错误,我们将覆盖大多数方法。放松地去接受这些在调试 C 程序中有用的方法,包括但不限于调试器使用,识别常见错误类型,陷阱和有效的 Google 搜索提示。
## 代码内调试
## 清洁代码
### 简化代码
使用辅助函数使代码模块化。如果有重复的任务(例如,获取指向 MP2 中连续块的指针),请将它们作为辅助函数。并确保每个函数都能很好地完成一件事,这样您就不必再调试两次了。
......@@ -43,28 +43,96 @@ void selection_sort(int *a, long len);
最后,我们不是关于重构/调试代码的类 - 事实上,大多数系统代码都是如此残酷,以至于您不想阅读它。但是为了调试,从长远来看,采用某些做法可能会对您有所帮助。
## 断言!
### 断言!
使用断言来确保您的代码在某一点上起作用 - 更重要的是,确保您以后不要破坏它。例如,如果您的数据结构是双向链表,您可以执行类似断言(node-&gt; size == node-&gt; next-&gt; prev-&gt; size)来断言下一个节点有一个指向当前节点的指针。您还可以检查指针指向预期的内存地址范围,而不是 null, - &gt;大小是合理的等.NDEBUG 宏将禁用所有断言,因此在完成调试后不要忘记设置它。 [http://www.cplusplus.com/reference/cassert/assert/](http://www.cplusplus.com/reference/cassert/assert/)
使用断言来确保您的代码在某一点上起作用 - 更重要的是,确保您以后不要破坏它。例如,如果您的数据结构是双向链表,您可以执行`assert(node -> size == node -> next -> prev -> size)`来断言下一个节点有一个指向当前节点的指针。您还可以检查指针指向预期的内存地址范围,而不是 `null``->size`是合理的等等。NDEBUG 宏将禁用所有断言,因此在完成调试后不要忘记设置它。 [http://www.cplusplus.com/reference/cassert/assert/](http://www.cplusplus.com/reference/cassert/assert/)
断言的一个简单例子就是说我正在使用 memcpy 编写代码
断言的一个简单例子,我正在使用 `memcpy` 编写代码
```c
assert(!(src < dest+n && dest < src+n)); //Checks overlap
assert(!(src < dest+n && dest < src+n)); // 检查溢出
memcpy(dest, src, n);
```
这个检查可以在编译时关闭,但会节省你**吨**的调试问题
这个检查可以在编译时关闭,但会节省你**成吨**的调试问题的时间
## 用 printfs
### 用 printfs
当所有其他方法都失败时,打印就像疯了你的每个函数都应该知道它将要做什么(即 find_min 更好地找到最小元素)。您希望测试每个函数是否正在执行它要执行的操作,并确切地查看代码中断的位置。在有竞争条件的情况下,tsan 可能会提供帮助,但让每个线程在特定时间打印出数据可以帮助您识别竞争条件。
当所有其他方法都失败时,打印是一个好选择!你的每个函数都应该知道它将要做什么(例如 find_min 更好地找到最小元素)。您希望测试每个函数是否正在执行它要执行的操作,并确切地查看代码中断的位置。在有竞争条件的情况下,`tsan` 可能会提供帮助,但让每个线程在特定时间打印出数据可以帮助您识别竞争条件。
## Valgrind
## Valgrind
(去做)
`Valgrind` 是一系列用来调试和收集信息的工具,可以使你的程序更加正确和发现一些运行时的问题。最有用的工具是`Memcheck`,它可以发现一些内存相关的错误,通常在c和c++ 里面常犯的、导致程序崩溃的和不能预测的行为(比如,没有释放内存缓存)。
## 赞
在你的程序上使用`valgrind`:
> valgrind --leak-check=yes myprogram arg1 arg2
或者
> valgrind ./myprogram
参数是可选的,默认运行工具是`memcheck`.输出将用表格的方式展示出来,包括内存的分配大小和释放内存的大小,错误的数量。
这里是一个例子来帮助你解释这些结果。假设你有一个简单的像这样的程序:
```c
#include <stdlib.h>
void dummy_function()
{
int* x = malloc(10 * sizeof(int));
x[10] = 0; // error 1:as you can see here we write to an out of bound memory address
} // error 2: memory leak the allocated x not freed
int main(void)
{
dummy_function();
return 0;
}
```
让我们看一下`Valgrind`的输出(这个程序编译和运行没有错误)。
```log
==29515== Memcheck, a memory error detector
==29515== Copyright (C) 2002-2015, and GNU GPL'd, by Julian Seward et al.
==29515== Using Valgrind-3.11.0 and LibVEX; rerun with -h for copyright info
==29515== Command: ./a
==29515==
==29515== Invalid write of size 4
==29515== at 0x400544: dummy_function (in /home/rafi/projects/exocpp/a)
==29515== by 0x40055A: main (in /home/rafi/projects/exocpp/a)
==29515== Address 0x5203068 is 0 bytes after a block of size 40 alloc'd
==29515== at 0x4C2DB8F: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==29515== by 0x400537: dummy_function (in /home/rafi/projects/exocpp/a)
==29515== by 0x40055A: main (in /home/rafi/projects/exocpp/a)
==29515==
==29515==
==29515== HEAP SUMMARY:
==29515== in use at exit: 40 bytes in 1 blocks
==29515== total heap usage: 1 allocs, 0 frees, 40 bytes allocated
==29515==
==29515== LEAK SUMMARY:
==29515== definitely lost: 40 bytes in 1 blocks
==29515== indirectly lost: 0 bytes in 0 blocks
==29515== possibly lost: 0 bytes in 0 blocks
==29515== still reachable: 0 bytes in 0 blocks
==29515== suppressed: 0 bytes in 0 blocks
==29515== Rerun with --leak-check=full to see details of leaked memory
==29515==
==29515== For counts of detected and suppressed errors, rerun with: -v
==29515== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)
```
**不合格的写入**:它发现我们的堆块超过限度了(在分配块外写入)
**绝对损失**:内存泄露——你可能忘记释放内存块了
Valgrind 是一个有效的工具来检查在运行时的错误。C特别存在这种问题,所以在编译你的程序之后你可以使用Valgrind来修复编译时未能捕获的和经常发生在运行时的错误。
更多的信息请参考![官网](http://valgrind.org/docs/manual/quick-start.html)
## Tsan
ThreadSanitizer 是 Google 的一个工具,内置于 clang(和 gcc)中,可帮助您检测代码中的竞争条件。有关该工具的更多信息,请参阅 Github wiki。
......@@ -118,7 +186,7 @@ ThreadSanitizer: reported 1 warnings
简介: [http://www.cs.cmu.edu/~gilpin/tutorial/](http://www.cs.cmu.edu/%7Egilpin/tutorial/)
#### 以编程方式设置断点
### 以编程方式设置断点
使用 GDB 调试复杂的 C 程序时,一个非常有用的技巧是在源代码中设置断点。
......@@ -126,7 +194,7 @@ ThreadSanitizer: reported 1 warnings
int main() {
int val = 1;
val = 42;
asm("int $3"); // set a breakpoint here
asm("int $3"); // 在这里设置一个断点
val = 7;
}
```
......
......@@ -13,19 +13,19 @@
* 堆内存寿命
* 调用堆分配
* 取消引用指针
* 运营商地址
* 操作符的地址
* 指针算术
* 字符串重复
* 字符串截断
*免费错误
*重释放错误
* 字符串文字
* 打印格式。
* 格式化打印
* 内存超出界限错误
* 静态记忆
* fileio POSIX v C 库
* 静态内存
* 文件io POSIX v C 库
* C io fprintf 和 printf
* POSIX 文件 io(读|写|打开)
* 缓冲 stdout
* stdout的缓冲
## 问题/练习
......@@ -49,7 +49,7 @@ char *str2 = "another one";
```
* c 中的字符串是什么?
* 编码一个简单的`my_strcmp``my_strcat``my_strcpy``my_strdup`怎么样?额外奖励:仅在中通过字符串 _ 编码功能。_
* 编码一个简单的`my_strcmp``my_strcat``my_strcpy``my_strdup`怎么样?额外奖励:编写的函数仅需遍历一遍字符串。
* 以下通常应该返回什么?
```c
......@@ -76,7 +76,7 @@ ptr[1] = malloc(20); //0x300
* `*((int)(ptr + 1)) + 3`
```
* 我们如何防止双重免费错误?
* 我们如何防止双重释放错误?
* 什么是打印字符串,`int``char`的 printf 说明符?
* 以下代码是否有效?如果是这样,为什么? `output`在哪里?
......@@ -88,5 +88,5 @@ char *foo(int var){
}
```
* 编写一个接受字符串并打开该文件的函数,一次打印出 40 个字节的文件,但每个其他打印文件都会反转字符串(尝试使用 POSIX API)。
* 编写一个函数,该函数接受一个字符串并打开该文件,第一次打印出文件的40个字节,但其他每次打印都会反转该字符串(请尝试使用POSIX API)
* POSIX filedescriptor 模型和 C `FILE*`之间有什么区别(即使用了哪些函数调用,哪些是缓冲的)? POSIX 是否在内部使用 C `FILE*`,反之亦然?
\ No newline at end of file
......@@ -2,7 +2,7 @@
> 原文:<https://github.com/angrave/SystemProgramming/wiki/C-Programming%2C-Part-1%3A-Introduction>
## 快速介绍一下 C?
## 快速认识一下 C?
* 请继续阅读下面的 C 速成课程
* 然后查看 [C Gotchas wiki 页面](https://cs241.apachecn.org/#/docs/10)
......@@ -36,13 +36,13 @@ int main(void) {
### 为什么我们使用'`#include <stdio.h>;`'?
我们很懒!我们不想声明`printf`函数。它已经在文件'`stdio.h`'中为我们完成了。 `#include`包含文件的文本作为我们要编译的文件的一部分。
我们很懒!我们不想声明`printf`函数。它已经在文件'`stdio.h`'中为我们完成了。 `#include`引入文件的文本内容将作为我们要编译的文件的一部分。
具体来说,`#include`指令采用文件`stdio.h`(它代表标准输入和输出)位于操作系统的某个位置,复制文本,并将其替换为`#include`所在的位置。
### C 字符串是如何表示的?
它们在内存中表示为字符。字符串的结尾包括 NULL(0)字节。所以“ABC”需要四(4)个字节`['A','B','C','\0']`。找出 C 字符串长度的唯一方法是继续读取内存,直到找到 NULL 字节为止。 C 字符总是恰好一个字节。
它们在内存中表示为字符。字符串的结尾包括 NULL(0)字节。所以“ABC”需要四(4)个字节`['A','B','C','\0']`。找出 C 字符串长度的唯一方法是继续读取内存,直到找到 NULL 字节为止。 C 字符在内存中总是恰好一个字节。
在表达式中编写字符串文字`"ABC"`时,字符串文字将计算为 char 指针(`char *`),该指针指向字符串的第一个字节/字符。这意味着下面示例中的`ptr`将保存字符串中第一个字符的内存地址。
......@@ -50,6 +50,14 @@ int main(void) {
char *ptr = "ABC"
```
下面是一些常用的初始化字符串的方式:
```c
char *str = "ABC"
char str[] = "ABC"
char str[] = {"A", "B", "C", "\0"};
```
### 你如何声明一个指针?
指针指的是内存地址。指针的类型很有用 - 它告诉编译器需要读/写多少字节。您可以按如下方式声明指针。
......@@ -90,15 +98,15 @@ char *ptr = "Hello"; // ptr 保存 'H'的内存位置
ptr += 2; //ptr 现在指向 'l'
```
如果 int 是 4 个字节,那么 ptr + 1 指向 ptr 指向的 4 个字节
如果 int 是 4 个字节,那么 ptr + 1 指向 ptr 指针往后移动 4 个字节的位置
```c
char *ptr = "ABCDEFGH";
int *bna = (int *) ptr;
bna +=1; // Would cause iterate by one integer space (i.e 4 bytes on some systems)
bna +=1; // 每次迭代一个int类型的内存大小(在某些机器上是4个字节)
ptr = (char *) bna;
printf("%s", ptr);
/* Notice how only 'EFGH' is printed. Why is that? Well as mentioned above, when performing 'bna+=1' we are increasing the **integer** pointer by 1, (translates to 4 bytes on most systems) which is equivalent to 4 characters (each character is only 1 byte)*/
/* 注意到只有“EFGH”打印出来了。这是为什么呢?就是我们刚刚提到的,当操作“bna+=1"时,我们对int指针增加了1(在大多数机器是4个字节),这就等于4个字符(每个字符一个字节)。
return 0;
```
......@@ -148,7 +156,7 @@ gcc 和 clang 不符合 ISO-C 标准,这意味着它们可以让你对 void
```c
int num1 = 10;
printf("%d", num1); //prints num1
printf("%d", num1); //打印num1的值
```
整数指针的示例:
......@@ -156,10 +164,9 @@ printf("%d", num1); //prints num1
```c
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
printf("%p\n", ptr); //打印指针指向的地址
printf("%p\n", &ptr); /* 打印指针本身的地址,在处理双指针的问题时非常有用 */
printf("%d", *ptr); //打印指针指向的地址的内容
return 0;
```
......@@ -168,7 +175,7 @@ return 0;
```c
char *str = (char *) malloc(256 * sizeof(char));
strcpy(str, "Hello there!");
printf("%p\n", str); // print the address in the heap
printf("%p\n", str); // 打印堆的地址
printf("%s", str);
return 0;
```
......@@ -222,10 +229,10 @@ strcpy(ptr, "World"); // NOT OK - Segmentation fault (crashes)
```c
ptr = "World"; // OK!
ptr = ary; // OK!
ary = (..anything..) ; // WONT COMPILE
// ary is doomed to always refer to the original array.
ary = (..anything..) ; // 不能通过编译
// ary只能指向原数组。
printf("%p : %s\n", ptr, ptr);
strcpy(ptr, "World"); // OK because now ptr is pointing to mutable memory (the array)
strcpy(ptr, "World"); // OK, ptr现在指向可变的内存(数组)
```
从中可以看出,指针*可以指向任何类型的内存,而 C 数组[]只能指向栈上的内存。在更常见的情况下,指针将指向堆内存,在这种情况下,指针 CAN 引用的内存可以被修改。
......@@ -251,14 +258,14 @@ sizeof(str2) //8 因为它是一个指针
int* f1(int *p) {
*p = 42;
return p;
} // This code is correct;
} // 正确;
```
```c
char* f2() {
char p[] = "Hello";
return p;
} // Incorrect!
} // 错误!
```
说明:在栈上创建一个数组 p,其大小正确以容纳 H,e,l,l,o 和空字节,即(6)字节。这个数组存储在栈中,从 f2 返回后无效。
......@@ -297,7 +304,7 @@ int *space = malloc(sizeof(int) * 10);
```c
void mystrcpy(char*dest, char* src) {
// void means no return value
// void 表示没有返回值
while( *src ) { dest = src; src ++; dest++; }
}
```
......@@ -345,9 +352,9 @@ free(n);
int *p = malloc(sizeof(int));
free(p);
*p = 123; // Oops! - Dangling pointer! Writing to memory we don't own anymore
*p = 123; // Oops! - 危险的指针! 写入了不存在的内存
free(p); // Oops! - Double free!
free(p); // Oops! - 二次释放!
```
要修复首先是编写正确的程序!其次,一旦内存被释放,重置指针是很好的编程风格。这可以确保指针无法正确使用而不会导致程序崩溃。
......@@ -355,12 +362,12 @@ free(p); // Oops! - Double free!
修复方法:
```c
p = NULL; // Now you can't use this pointer by mistake
p = NULL; // 现在你不会犯错了使用这个指针
```
### 什么是缓冲区溢出的例子?
着名的例子:Heart Bleed(在一个大小不足的缓冲区中执行 memcpy)。简单的例子:在确定所需内存的大小时,实现 strcpy 并忘记添加一个 strlen
着名的例子:Heart Bleed(在一个大小不足的缓冲区中执行 memcpy)。简单的例子:在确定所需内存的大小时,实现 strcpy 时忘记对strlen 添加 1
### 什么是'typedef',你如何使用它?
......@@ -390,6 +397,6 @@ comparator gt = greater_than;
这声明了一个函数类型比较器,它接受两个`void*`参数并返回一个 int。
### 哇 C 好多啊
### 哇 这就足够理解C语言了
不要再担心了!
不要再担心接下来的知识了!
......@@ -4,7 +4,7 @@
## 打印到流
## 如何将字符串,整数,字符打印到标准输出流?
### 如何将字符串,整数,字符打印到标准输出流?
使用`printf`。第一个参数是格式字符串,其中包含要打印的数据的占位符。通用格式说明符是`%s`将参数视为 c 字符串指针,保持打印所有字符,直到达到 NULL 字符; `%d`将参数打印为整数; `%p`将参数打印为内存地址。
......@@ -14,25 +14,25 @@
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
// name已经是一个字符串指针,指向字符串的开头第一个字节的地址。
// 我们需要用“&”操作符来获取整数变量score的地址。
```
默认情况下,为了提高性能,`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 字符串中?
### 如何将数据打印到 C 字符串中?
使用`sprintf`或更好`snprintf`
......@@ -41,9 +41,17 @@ char result[200];
int len = snprintf(result, sizeof(result), "%s:%d", name, score);
```
snprintf 返回写入的字符数,不包括终止字节。在上面的例子中,这最多为 199
~~snprintf 返回写入的字符数,不包括终止字节。在上面的例子中,这最多为 199。~~snprintf 返回有足够的空间写入字符串的长度,不包括末尾NULL字节
## 如果我真的希望`printf`在没有换行符的情况下调用`write`怎么办?
```c
char x[5];
int size = snprintf(x, 5, "%s%s%s", "12", "34", "56"); // writes "1234" + null
printf("%d\n", size); // output 6
```
来源: ![this StackOverflow post](https://stackoverflow.com/questions/12746885/why-use-asprintfand) 和 手册页。
### 如果我真的希望`printf`在没有换行符的情况下调用`write`怎么办?
使用`fflush( FILE* inp )`。将写入文件的内容。如果我想写没有换行的“Hello World”,我可以像这样写。
......@@ -55,7 +63,7 @@ int main(){
}
```
## `perror`如何帮助?
### `perror`如何帮助?
假设您有一个失败的函数调用(因为您检查了手册页,它是一个失败的返回码)。 `perror(const char* message)`会将错误的英文版本打印到 stderr
......@@ -72,7 +80,7 @@ int main(){
## 解析输入
## 如何从字符串中解析数字?
### 如何从字符串中解析数字?
使用`long int strtol(const char *nptr, char **endptr, int base);``long long int strtoll(const char *nptr, char **endptr, int base);`
......@@ -87,22 +95,22 @@ int main(){
}
```
但要小心!错误处理有点棘手,因为该函数不会返回错误代码。出错时,它将返回 0,你必须手动检查 errno,但这可能会导致麻烦
但要小心!错误处理有点棘手,因为该函数不会返回错误代码。如果你传入一个会返回0的字符串而不是一个数字,这意味着你不能区别出一个合格的“0”和一个不合格的字符串。查看参考手册页去获得更多的关于strtol的不合法行为和超出边界的值。一个安全的替代是使用`sscanf`(并且检查返回值)
```c
int main(){
const char *zero = "0";
const char *input = "0"; // or "!##@" or ""
char* endptr;
printf("Parsing number"); //printf sets errno
long int parsed = strtol(num, &endptr, 16);
long int parsed = strtol(input, &endptr, 10);
if(parsed == 0){
perror("Error: "); //oops strtol actually worked!
// 不管输入的字符串是一个合格的10进制数还是真的是0
}
return 0;
}
```
## 如何使用`scanf`将输入解析为参数?
### 如何使用`scanf`将输入解析为参数?
使用`scanf`(或`fscanf``sscanf`)分别从默认输入流,任意文件流或 C 字符串获取输入。检查返回值以查看解析了多少项是个好主意。 `scanf`函数需要有效的指针。传递不正确的指针值是常见的错误来源。例如,
......@@ -110,13 +118,13 @@ int main(){
int *data = (int *) malloc(sizeof(int));
char *line = "v 10";
char type;
// Good practice: Check scanf parsed the line and read two values:
// 好习惯:确保scanf解析了line并读取了两个值
int ok = 2 == sscanf(line, "%c %d", &type, &data); // pointer error
```
我们想将字符值写入 c,将整数值写入 malloc 内存。但是我们传递了数据指针的地址,而不是指针指向的地址!所以`sscanf`会改变指针本身。即,指针现在将指向地址 10,因此该代码稍后将失败,例如当调用 free(数据)时。
## 如何阻止 scanf 导致缓冲区溢出?
### 如何阻止 scanf 导致缓冲区溢出?
以下代码假定 scanf 不会将超过 10 个字符(包括终止字节)读入缓冲区。
......@@ -129,16 +137,16 @@ scanf("%s",buffer);
```c
char buffer[10];
scanf("%9s", buffer); // reads upto 9 charactes from input (leave room for the 10th byte to be the terminating byte)
scanf("%9s", buffer); // 读取至多9个字符从输入(第十个字节是留给终止字节的)
```
## 为什么`gets`很危险?我应该用什么呢?
### 为什么`gets`很危险?我应该用什么呢?
以下代码容易受到缓冲区溢出的影响。它假定或信任输入行不超过 10 个字符,包括终止字节。
```c
char buf[10];
gets(buf); // Remember the array name means the first byte of the array
gets(buf); // 请记住数组名代表着数组的第一个字节
```
`gets`在 C99 标准中已弃用,已从最新的 C 标准(C11)中删除。程序应使用`fgets``getline`代替。
......@@ -168,28 +176,27 @@ if (buffer[i] == '\n')
buffer[i] = '\0';
```
## 我该如何使用`getline`?
### 我该如何使用`getline`?
`getline`的一个优点是将在足够大小的堆上自动(重新)分配缓冲区。
```c
// ssize_t getline(char **lineptr, size_t *n, FILE *stream);
/* set buffer and size to 0; they will be changed by getline */
/* 初始化buffer和size的值,它们会被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);
```
\ No newline at end of file
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册