前言:C程序设计语言(第2版·新版)--读书笔记
0X00 运行第一个程序
1.创建test.c程序
#include <stdio.h>
void main()
{
printf("hello world\n");
}
2.编译程序
cc test.c
3.编译完成后会输出编译后的结果:a.out,然后进行执行
./a.out
4.执行结果
生成的a.out为很老的输出格式了,建议输出ELF文件:
gcc -o test test.c
/* 运行: */
/* ./test */
5.C语言中的main函数
main是一个特殊的函数名,每个程序都从main函数的起点开始执行,这意味着每个程序都必须在某个位置包含一个main函数
6.prinf不会自动换行,如果多行printf并且不携带\n的转义字符,那么内容都将显示在同一行
0X01 变量与算数表达式
1.华氏摄氏度与摄氏摄氏度转换
#include <stdio.h>
/*华氏摄氏度与摄氏摄氏度转换表*/
main()
{
int fahr, celsius;
int lower, upper, step;
lower = 0;
upper = 300;
step = 20;
fahr = lower;
while (fahr <= upper) {
celsius = 5 * (fahr - 32) / 9;
printf("%d\t%d\n", fahr, celsius);
fahr += step;
}
}
2.字符对齐
printf("%3d %6d", a, b)
/* 表示第一个参数占三个数字宽,第二个参数占6个数字宽,右对齐 */
3.for循环
int i;
for (i = 0; i <= 10 ; i = i + 2)
printf("%d\n", i);
/*
注意两点:
1.printf不能直接打印除str以外的变量
2.for循环只执行一条语句时可以不带花括号,如果要执行一组语句则必须携带花括号
*/
4.符号常量
如果直接在代码中定义a=30,b=40,那么这个30和40可能是表意不清的,所以推荐使用符号常量进行定义,该符号常量不会发生变化,固定为一个值。
#include <stdio.h>
#define LOWER 0
#define UPPER 300
#define STEP 20
5.字符输入/输出
字符在键盘、屏幕或其他任何形式展现,它在机器内部都是以位的模式存储的,
getchar()会读取用户输入的下一个字符串,putchar()会打印输入的字符。getchar()返回的为ASCII码值,所以用int类型接收,C语言设定当没有输入时,getchar()会返回一个特殊值,这个特殊值和实际字符不同,EOF(END OF FILE)
main()
{
int c;
c = getchar();
while (c != EOF){
putchar(c);
c = getchar();
}
}
6.C语言中的char存放的是ASCII码值,所以是可以将char类型与int类型之间执行加减乘除操作的,实质就是将ASCII码值进行加减乘除
main()
{
int a = 3;
char b = 'a';
printf("%d", (b - a)); /* 94 */
}
7.C语言中的函数传值全部是值传递,不存在引用传递。例如n=1,n的内存地址为12,传递到函数中会开辟一个临时空间用来存n的值,内存地址可能为13。如果需要修改原参数的值,可以通过指针进行操作。
8.不存在字符串这个定义,多个字符将用字符数组进行存储,数组的各元素分别存储字符串的各个字符,并以 \0 标志字符串的结束:
例如存放 ‘hello\n’:
9.倒序一个字符串
WARNING:可以通过strlen方法获取字符串长度,但是字符串长度等于声明的字符串长度加一,因为字符数组末尾有个 \0
#include <stdio.h>
#include <string.h>
/*华氏摄氏度与摄氏摄氏度转换表*/
int power(int m, int n);
main()
{
char a[5] = "abcde";
for (int i = 0, j = strlen(a) - 1; i < j; i++, j--){
char tmp = a[i];
a[i] = a[j];
a[j] = tmp;
};
printf("%s", a);
};
10.函数内部变量与外部变量
- 函数外部定义的变量,如果要在函数内部使用需要加上extern的前缀,如果是同一个文件中定义的外部变量,那么可以省略extern前缀
- 如果某个变量在file1中被定义,在file2和file3中被使用,那么在file2和file3中就需要使用extern前缀来定义该变量,建立该变量和file1中变量的关系,某则会被视为本地文件新定义的一个变量
- 通常会把变量和函数的extern声明放在一个单独的文件,习惯称为头文件。然后可以通过include将头文件进行导入
头文件的定义其实并不需要和.c文件名保持一致,并不是通过名字去查找源文件,而是和程序编译器工作原理有关
0X01 类型、运算符和表达式
1.数据类型和长度
char 字符型,占一个字节,存放一个字符
int 整型
float
double
short和long,short和int至少为16位,long至少为32位。且short不大于int长度,long不小于int长度。
类型限定符:signed和unsigned,用于限定char类型和任何整型。
关于字符串常量:需要注意的是,字符和字符串常量是两个概念,比如说定义一个字符使用单引号,定义字符串常量使用双引号:
char a = 'a';
char b[] = "this is a string"
C使用字符数组来存储字符串,所以字符串常量定义如下:
char a[] = "this is a string";
printf("%s", a)
2.const
通过const限定符限定的变量无法被修改:
const double e = 2.1738;
如果const作用于数据,那么数组中的所有元素都无法修改:
const char location[] = "Los Angles";
3.运算符
运算符优先级:
4.类型转换
当一个运算符的几个数据类型不同时,需要对数据类型进行统一,也就是类型转换,转换规则为将"窄"的数据类型转换为"宽"的数据类型,例如int和float相加,那么应该都转为float在进行运算。
可以将char类型转为int类型,但是需要注意一些问题:
C语言在很多情况下会进行隐式数据转换,具体转换规则例如:
当无符号类型与有符号类型比较时要格外注意,因为存在一些转换规则会影响我们的判断,感觉最好的方式是统一改为有符号数据类型在进行操作:
强制类型转换:
#include<math.h>
sqrt((double) n);
5.自增与自减运算符
n++和++n都是表示将n的值加一,但是++n表示先把n加一然后再使用n的值,而n++表示先使用n的值在把n加一,举个例子:
int n = m = 5;
int x, y;
x = ++n
y = m++;
在执行上述代码后,m和n的值都为5,但是x为6,y为5,这就是先后使用n的值的区别。
6.条件表达式
0X02 控制流
花括号后并不需要加分号
1.条件判断
if-else(省略花括号):
int a,b,c;
a = 1;
b = 2;
if (a > b)
printf("%d", a);
else
printf("%d", b);
涉及到多层if-else嵌套需要使用花括号,否则else都会与最近的一个if结构进行配对。
main(){
int a,b,c;
a = 2;
b = 1;
if (a > b) {
printf("%d", a);
if (a == b)
printf("%d:%d", a, b);
}
else
printf("%d", 3);
}
2.switch-case语句
多路判断语句,可以设置多个分支动作。需要注意的是,case语句只是提供跳转功能,跳转到满足要求的case语句后程序继续往下执行,例如下面的程序将会打印出 "112Done",因为在case 1中没有加入break语句,程序跳转到case 1之后就把后续的语句全部进行执行了。
main(){
int a,b,c;
a = 2;
b = 1;
c = a - b;
printf("%d", c);
switch (c){
case 1: printf("%d", 1);
case 2: printf("%d", 2);
default: printf("Done");
}
}
所以如果执行执行某个case后的语句,需要加入break语句:
main(){
int a,b,c;
a = 2;
b = 1;
c = a - b;
printf("%d", c);
switch (c){
case 1:
printf("%d", 1);
break;
case 2: printf("%d", 2);
default: printf("Done");
}
}
3.do-while
先执行后进行条件判断,循环体至少被执行一次
do{
printf("%s", "this is a test");
}
while(2 > 1);
4.goto
跳转到指定标记处,在合适的情况下可以使用,不要滥用
main(){
for (int i = 0; i < 10; i++){
if(i == 5){
printf("%d", i);
goto done;
}
}
done: printf("%s", "Done");
}
0X03 函数与程序结构
函数声明类型与返回类型需保持一致,否则会被强制转换成函数声明类型,可能会导致数据丢失
在头部的函数声明只需要指定参数类型即可
int func(int, int);
int func(int x, int y){
}
1.内部变量与外部变量
内部变量只在函数运行时存在,函数运行结束内部变量也就被释放了。
外部变量可以在全局范围内使用,任何函数都可以访问同一个外部变量,那么这也是各个函数之间数据交换的一种方式,不限于只通过函数参数来进行数据交换。如果两个函数互相不会调用但是又需要进行数据交换,那么使用外部变量是一种很好的方式。
#include<stdio.h>
char test[10] = "tetete";
int main(){
test[0] = 'b';
printf("%s", test);
}
2.作用域规则
如果需要使用其他源文件的外部变量,则必须使用extern关键字
include头文件相当于把头文件内容复制到了该处
可以通过static关键字将变量声明为静态变量,防止其他文件进行访问
extern int sp;
static int a;
3.寄存器变量
4.C预处理器
(1)文件包含
/* 第一种方式,根据相应规则查找*/
#include <test.h>
/* 第二种方式,在源文件目录下查找该文件*/
#include "test.h"
(2)宏替换
#define MAX 100
/* 也可以替换函数 */
#define forever for(;;)
(3)条件包含
可以避免重复包含
#ifndef HDR
#define HDR "sys.h"
#endif
#include <HDR>
0X04 指针和数组
1.指针
指针实际上存储的是变量的内存地址,通常占2-4字节。可以使用一元运算符&用于取变量的地址:
p = &c;
地址运算符&只能用于内存中的对象,即变量与数组,不能用于表达式、常量或register类型的变量
一元运算符 * 是间接寻址或间接引用运算符,当它作用于指针时将访问指针指向的对象。
指针的声明:
int *p;
/* 声明一个int类型的指针,表示该指针将指向一个int类型的变量
并且通过 *p访问指针指向的对象,将会得到一个int类型数据 */
指针也是变量,可以直接赋值给另一个指针变量,但不能直接赋值给另一个普通变量
2.指针与函数
1)由于函数传参都是值传递,所以以下代码无法交换a,b的值:
#include<stdio.h>
int *a;
int swap(int x, int y);
int main(){
int a = 10;
int b = 20;
swap(a, b);
printf("%d", a);
printf("%d", b);
}
int swap(int x, int y){
int temp = x;
x = y;
y = temp;
}
2)如果传递的是a,b的指针则可以交换a,b的值:
#include<stdio.h>
int *a;
int swap(int *x, int *y);
int main(){
int a = 10;
int b = 20;
swap(&a, &b);
printf("%d", a);
printf("%d", b);
}
int swap(int *x, int *y){
int temp = *x;
*x = *y;
*y = temp;
}
3)目前暂时不知道如何声明字符串指针,但是可以声明针对字符串中某个元素的指针
#include <stdio.h>
#include <ctype.h>
int getint(char *x);
int main(){
char p[] = "abcdefghj";
int result;
char *y;
y = &p[0];
result = getint(&p[0]);
printf("%d", result);
printf("%d", *y);
}
int getint(char *x){
if (*x == EOF)
return 0;
else
return 1;
}
3.指针和数组
1)指针和数组的关系非常密切,int a[10]相当于定义了一个10个对象组成的集合,在内存中为一连续的存储区域,顺序存储了这10个对象。
a[i]表示该数组的第i个元素,可以声明一个指针指向它:
int a[10];
int *pa;
pa = &a[0];
pa指针指向了该数组的第0个元素,那么pa+1将会指向该数组的第1个元素。这就涉及到了指针的算数运算,例如int a[10],数组中每个对象都会占用一个字节,当把pa指向了a[0]后,pa+1会将pa的地址往右移动一个字节,那么就指向了a[1]。所以如果将p指针指向了数组任意一个元素,p+i都会指向该元素往后的第i个元素。
#include <stdio.h>
#include <ctype.h>
int main(){
char a[] = "abcdffe";
char *p;
p = &a[0];
printf("%d", *(p + 1));
}
概括一下:
- 指针的每一次递增,它其实会指向下一个元素的存储单元。
- 指针的每一次递减,它都会指向前一个元素的存储单元。
- 指针在递增和递减时跳跃的字节数取决于指针所指向变量数据类型长度,比如 int 就是 4 个字节。
那么我们对于数组元素的引用可以使用 *(p + i)的形式,因为C在处理a[i]这种访问时,实际上是先将a[i]转换为*(a + i)的形式。
2)数组指针和数组名
char a[10]的数组名为a,a实际上存储的是数组第0个元素的内存地址,那么:p == a是成立的
#include <stdio.h>
#include <ctype.h>
int main(){
char a[] = "abcdffe";
char *p;
p = &a[0];
if(p == a)
printf("1");
else
printf("0");
}
3)函数传参之数组
函数接收数组类型的参数传递的实际上数组第一个元素的地址,所以如果要声明传递数组元素的函数,以下两种方式都是正确的:
#include <stdio.h>
#include <ctype.h>
/* int test(char a[]); */
int test(char *a);
int main(){
char a[] = "abcdffe";
test(a);
}
int test(char *a){
printf("%d", a[0]);
return 0;
}
4)一个地址分配的DEMO
#include <stdio.h>
#define MAX_SIZE 10000 /* 定义buf最大空间 */
static char t_buf(MAX_SIZE); /* 创建buf */
static char *start = t_buf; /* 指向初始位置 */
char *use_space(int n); /* 申请n个字符的空间 */
void *free_space(char *p); /* 释放指针p指向的存储区 */
char *use_space(int n){
if(t_buf + MAX_SIZE - start >= n){
return start;
}
else
return 0;
}
void *free_space(char *p){
if(p > t_buf && p < t_buf + MAX_SIZE){
start = p;
}
}
4.指针数组和指向指针的指针
1)指针数组
例如一篇文章有多段话,那么可以将每段话的起始地址存在一个指针中,如果需要交换段落的位置只需要改变他们指针的指向即可,而不需要改变他们的存储位置。
char *p[100];
/* 定义了一个具有100个对象的数组,数组中的每个对象为指向字符串的指针,这就是指针数组 */
同类型的指针可以相减,其结果是两个指针所指向地址间相差的这个类型元素的个数,如果是高地址的减低地址就是正数,否则就是负数
2)指针数组的初始化
5.命令行参数
argv[0]是函数的程序名,因此argv的值至少为1,argv[last]最后一个未传值的参数一定是个空指针
6.指向函数的指针
int (*comp)(void *, void*);
/* 定义了一个指向函数的指针,comp表示指向该函数的指针,使用*comp即访问的该指针指向的函数 */
/* 注意和返回int指针的函数的区别 */
/* int *comp(void *, void*); */
0X05 Struct
1.基础
struct point {
int x;
int y;
};
结构体的定义需要携带分号。
可以通过以下形式访问结构体参数:
int a,b;
a = b = 4;
struct point p = {a, b};
printf("%d", p.x);
多重结构体嵌套:
struct rectangle{
struct point pt1;
struct point pt2;
}
struct rectangle rec = {};
rec.pt1.x;
结构体的长度可能不等于结构体各个成员长度之和,使用sizeof返回一下最靠谱。
2.结构体与函数
返回结构体的函数(参数也是两个结构体),需要注意的是时刻需要注明结构体的类型,再就是返回结构体的函数需要在结构声明后声明:
#include<stdio.h>
struct point{
int x;
int y;
};
struct rectangle{
struct point p1;
struct point p2;
};
struct rectangle make_rec(struct point pt1, struct point pt2){
struct rectangle rec = {pt1, pt2};
return rec;
};
int main(){
struct point pt1 = {3, 4};
struct point pt2 = {4, 5};
struct rectangle rec;
rec = make_rec(pt1, pt2);
printf("%d", rec.p1.x);
}
结构体传递同样是值传递,除非传递结构体指针,例如下面的例子:
直接返回p1就是强调结构体同样是值传递。
3.结构体指针
如果要传递给函数的结构体很大,那么使用指针效率更高
struct point *pp;
需要注意的是'.'的优先级大于'*',所以如果要通过结构体指针访问结构体成员,需要加上括号:
(*pp).x;
提供了更简洁的使用方式:'->'
/* p->x; */
int main(){
struct point pt1 = {3, 4};
struct point *p = &pt1;
printf("%d", p->y);
}
由于'->'运算符优先级大于'++',所以以下代码是先访问p的成员再执行加一的操作:
++p->x;
4.结构数组
也就是存储了N个结构体的数组
struct key{
char *word;
int count;
};
struct key keywords[MAX_SIZE];
/* 另一种写法 */
struct key{
char *word;
int count;
} keywords[MAX_SIZE];
结构数组的初始化:
/* 如果初始值是简单变量且不为空,那么可以省略括号 */
struct key{
char *word;
int count;
} keywords[] = {
"break", 0,
"if", 0,
"else", 0,
"void", 0,
"while", 0,
};
/* 标准写法需要带上括号 */
struct key{
char *word;
int count;
} keywords[] = {
{"break", 0},
{"if", 0},
{"else", 0},
};
5.自引用结构
一个包含自身结构体实例的结构体是违法的,但是包含自身结构体实例的指针的结构体是合法的,例如:
/* 非法 */
struct node{
char *word;
int count;
struct node left_n;
struct node right_n;
};
/* 合法 */
struct node{
char *word;
int count;
struct node *left_n;
struct node *right_n;
};
6.类型定义(typedef)
给某个数据类型起别名,例如:
typedef struct tnode{
char *word;
int count;
} Treenode;
typedef类似于define,但是typedef是由解释器解释的,而define工作在预处理器阶段
7.联合(union)
当一个变量可能是多种不同的数据类型时,就可以使用union进行存储,合法保存多种数据类型中任何一种类型的对象。变量u必须足够大,能够存储union中任意一种类型的数据。
union u_tag{
int ival;
char cval;
char *cpval;
} u;
union的访问方式和结构体一样,通过 '.' 或者 '->'进行访问
union可以和struct嵌套使用:
struct test{
int age;
char name[];
union {
int iz;
char cz;
} u;
};
union只能使用第一个成员类型的值进行初始化!
0X06 输入输出
1.从输入读取字符,将英文字符转换为小写并返回
tolower函数将大写英文字符转换为小写,其他字符原样返回
#include <stdio.h>
#include <ctype.h>
int main(){
char input;
while((input = getchar()) != EOF){
putchar(tolower(input));
}
return 0;
}
2.读取与输出
./b > b.txt /* 将程序输出重定向到b.txt */
./b < b.txt /* 程序从文件读取输入而不是键盘 */
./b | process /* 将程序输出通过管道传递给process的输入 */
3.文件访问
在读写文件前通过fopen打开文件,fopen会返回一个用于文件读写的指针,该指针称为文件指针,它指向了一个文件信息的结构。
#include <stdio.h>
int main(){
FILE *fp;
fp = fopen("/home/zcs_c/b.txt", "r");
char a;
a = getc(fp);
fclose(fp);
printf("%d", a);
}
/* int getc(FILE *fp) 从fp的输入流中读取一个字符 */
/* int putc(int c, FILE *fp) 将字符c写入到fp指向的文件中并返回写入的字符,如果写入失败返回EOF */
运行一个程序时,操作系统会打开3个文件并将这3个文件的指针交给该程序,分别是stdin,stdout,stderr,在大多数情况stdin指向键盘,stdout和stderr指向显示器,可以通过人工干预将stdout和stderr重定向文件或者管道。
1)行输入与输出
char *fgets(char *line, int maxline, FILE *fp);
/* 从fp指向的文件中读取下一个输入行,包括换行符和字符串结束标志EOF,并将其放在字符数组line中 */
如果遇到文件结尾或者错误返回NULL。
int fputs(char *s, int n, FILE *iop);
/* 将一行写入到文件中,如果发生错误返回EOF,否则返回一个非负值 */
2)字符串操作函数
4.存储管理函数
malloc函数和calloc函数,malloc函数用于申请一个n字节的长度未初始化存储空间,里面填充的是垃圾数据。
calloc申请一个空闲空间,该空间大小为 n*size字节,相当于能存储n个对象,每个对象占用size字节的数组,并且数组初始化为全0。
void *malloc(size_t n); /* 分配成功返回指针,否则NULL */
void *calloc(size_t n, size_t size); /* 划分成功返回指针,否则NULL */
free (p) /* 释放p指向的内存空间 */
DEMO:
1)使用前需要手动声明该函数 2)需要对存储对象的数据类型进行类型转换
#include <stdio.h>
#include <stdlib.h>
void *calloc(size_t n, size_t size);
int main(){
int *ip;
ip = (int *) calloc(10, sizeof(int));
printf("%d", *ip);
free(ip);
}
0X07 UNIX系统接口
UNIX系统中一切皆文件,所有的外设包括键盘显示器都被看作文件系统中的文件,因此所有的输入和输出都要通过读文件或写文件完成,也就是说通过一个单一接口可以处理外设和程序之间的所有通信。
在读写文件之前需要打开文件,如果打开成功系统会返回一个小的非负整数,该整数称为文件描述符,对文件的操作都通过文件描述符来标识文件。当shell运行一个程序时,将打开三个文件,对应的文件描述符分别是0,1,2,依次对应stdin,stdout,stderr。
1.低级IO,read&write
输入和输出是通过read&write的系统调用来实现的:
int n_read = read(int fd, char *buf, int n);
int n_written = write(int fd, char *buf, int n);
/* fd:文件描述符, buf:需要读或者写的字符数组, n:要传输的字节数 */
#include <stdio.h>
#include <unistd.h>
int main(){
char buf[1024];
int read_return;
while((read_return = read(0, buf, 1024)) > 0){
write(1, buf, read_return);
}
return 0;
}
2.open,create,close,unlink
1)open和fopen相似,不过open返回的是文件描述符,int类型的数值,而fopen返回的是一个文件指针。并且open相当于系统层面的系统调用,而fopen是C语言层面的库调用。
#include <fcntl.h>
#include <stdio.h>
int main(){
int fd;
fd = open("/home/zcs_c/b.txt", O_RDONLY, 0);
printf("%d", fd); /* 3 */
}
每次读取或者写入的数据可以为任意大小,最常用的值是1,但也这也意味着读取和写入的次数将会非常多,可以使用1024这样的与外设的物理块对应的大小,减少读写次数。