C语言学习摘要
C 语言学习摘要
0x00 常量与变量
1、标识符
只能以下划线、大小写字母、数字进行随机组合命名。不允许出现标点符号等特殊字符。
C 语言大小写敏感
需要注意的几个关键字:
关键字 | 说明 |
---|---|
extern | 声明变量或函数是在其它文件或本文件的其他位置定义 |
register | 声明寄存器变量 |
auto | 声明自动变量 |
goto | 无条件跳转语句 |
const | 定义常量,如果一个变量被 const 修饰,那么它的值就不能再被改变 |
enum | 声明枚举类型 |
volatile | 说明变量在程序执行中可被隐含地改变 |
2、关于变量定义的两三事
对于在创建初期就赋予初值的变量便不用多说,没有初始化的全局变量在声明过后会隐式的进行初始化为默认的初始值,局部变量不会自动初始化
数据类型 | 初始化默认值 |
---|---|
int | 0 |
char | '\0' |
float | 0 |
double | 0 |
pointer | NULL |
举个例子:
addtwonum.c
#include <stdio.h>
/*外部变量声明*/
extern int x ;
extern int y ;
int addtwonum()
{
return x+y;
}
test.c
#include <stdio.h>
/*定义两个全局变量*/
int x=1;
int y=2;
int addtwonum();
int main(void)
{
int result;
result = addtwonum();
printf("result 为: %d\n",result);
return 0;
}
$ gcc addtwonum.c test.c -o main
$ ./main
result 为: 3
可以看到,当在文件中使用到外部定义的变量时,只需要在本文件使用的地方加上 extern
即可,这样可以防止变量在不同文件中的重复定义。
3、常量
常量的定义可以使用 #define
或者 const
,他们的定义方式如下:
// #define 实例
#define identifier value
#define NUM 5; // 使用 #define 时无需指定数据的类型,但同样的,定义好的常量也是不允许被更改的
// const 实例
const type variable = value;
const int NUM = 5; // 正确,const相当于 Java 中的 final 关键字,由他做限定的常量是不允许被更改的,并且必须在定义的时候进行显式赋值
// const 错误使用方式示例
const int NUM;
NUM = 5; // 错误
const int NUM; // 错误
因为常量的特殊性,在定义时应该对其进行区分,所以应该使用大写字母单词的方式来命名常量
0x01 C 存储类
存储类中像我们上面讲过的 extern
,他们的作用可以类比为 Java 中的权限修饰符,他们定义 C 程序中变量/函数的范围(可见性)和生命周期。这些说明符放置在它们所修饰的类型之前。下面列出 C 程序中可用的存储类:
-
auto
auto 是所有局部变量默认的存储类,auto 只能用在函数中,也即是 auto 只能修饰局部变量
-
register
register 存储类用于定义存储在寄存器中而不是 RAM 中的局部变量。这意味着变量的最大尺寸等于寄存器的大小(通常是一个词),且不能对它应用一元的 '&' 运算符(因为它没有内存位置)。
{ register int miles; }
寄存器只用于需要快速访问的变量,比如计数器。还应注意的是,定义 'register' 并不意味着变量将被存储在寄存器中,它意味着变量可能存储在寄存器中,这取决于硬件和实现的限制。
-
static
static 的用法跟 Java 中一样,但因为 C 中并没有类的概念,所以所有的全局变量默认的都是 static 修饰,当局部变量使用 static 修饰时,他的生命周期将随着函数的调用而被创建,随着程序的消亡而被摧毁。但是局部变量的使用范围仍局限于函数内部,这个不会改变。
-
extern
除了上面讲过的 extern 可以修饰常量外,他也可以用来修饰全局变量以及函数,他的作用与上面定义常量时的一致。
0x03 运算符
运算符中需要特别说明的:
假设变量 A 的值为 60,变量 B 的值为 13
运算符 | 描述 | 实例 |
---|---|---|
~ | 取反运算符,按二进制位进行"取反"运算。运算规则:~1=-2; ~0=1; |
(~A ) 将得到 -61,即为 1100 0011,一个有符号二进制数的补码形式。 |
<< | 二进制左移运算符。将一个运算对象的各二进制位全部左移若干位(左边的二进制位丢弃,右边补0)。 | A << 2 将得到 240,即为 1111 0000 |
>> | 二进制右移运算符。将一个数的各二进制位全部右移若干位,正数左补0,负数左补1,右边丢弃。 | A >> 2 将得到 15,即为 0000 1111 |
杂项运算符
运算符 | 描述 | 实例 |
---|---|---|
& | 返回变量的地址。 | &a; 将给出变量的实际地址。 |
* | 指向一个变量。 | *a; 将指向一个变量。 |
? : | 条件表达式 | 如果条件为真 ? 则值为 X : 否则值为 Y |
0x04 函数
跟 Java 中不同的是,C语言中的函数在定义之前必须先声明,所谓的声明就是告诉编译器函数的名称、返回类型和参数。在函数声明的中可以不特别声明参数的名称,但是一定要表明参数的类型。
int max(int, int); // 这也是有效的声明
函数传参的类型:
调用类型 | 描述 |
---|---|
传值调用 | 该方法把参数的实际值复制给函数的形式参数。在这种情况下,修改函数内的形式参数不会影响实际参数。 |
引用调用 | 通过指针传递方式,形参为指向实参地址的指针,当对形参的指向操作时,就相当于对实参本身进行的操作。 |
0x05 枚举
第一个枚举成员的默认值为整型的 0,后续枚举成员的值在前一个成员上加 1。我们在这个实例中把第一个枚举成员的值定义为 1,第二个就为 2,以此类推。
可以在定义枚举类型时改变枚举元素的值:
enum season {spring, summer=3, autumn, winter};
没有指定值的枚举元素,其值为前一元素加 1。也就说 spring 的值为 0,summer 的值为 3,autumn 的值为 4,winter 的值为 5
#include <stdio.h>
#include <stdlib.h>
int main()
{
enum color { red=1, green, blue };
enum color favorite_color;
/* 用户输入数字来选择颜色 */
printf("请输入你喜欢的颜色: (1. red, 2. green, 3. blue): ");
scanf("%u", &favorite_color);
/* 输出结果 */
switch (favorite_color)
{
case red:
printf("你喜欢的颜色是红色");
break;
case green:
printf("你喜欢的颜色是绿色");
break;
case blue:
printf("你喜欢的颜色是蓝色");
break;
default:
printf("你没有选择你喜欢的颜色");
}
return 0;
}
0x06 C 中的指针
0x07 C 语言中的字符串
一般把字符串存放于字符数组中时,一定要存储字符串结束符 '\0',因为 C 库函数中,对字符串处理的函数,几乎都是把 '\0' 作为字符串结束标志的。如果字符数组中没有存储结束符,却使用了字符串处理函数,因为这些函数会寻找结束符 '\0',可能会产生意想不到的结果,甚至程序崩溃。例如:
char s1[5]="hello"; //s1不含'\0'
char s2[] = {'w','o','r','l','d'}; //s2大小:5,不含'\0'
char s3[5]; //未初始化,5个空间全为不确定值
s3[0] ='g';
s3[1]='o'; // 此时 s3 = {'g','o',?,?,?}
puts(s2); //s2中不含'\0',输出不确定值,甚至程序崩溃.
strcpy (s1, s3) ; //运行时错误。s3中找不到结束符'\0'
char s4[5] = {'g','o'}; // s4 = {'g','o','\0','\0','\0'}
puts (s4); //输出go并换行
strcpy (s1,s4); //把 s4 中的串 go 和一个'\o'复制到 s1 中。
// 此时 s4 = {'g','o','\0',l,o}
// 这时当字符串处理函数进行处理时,遇到第一个 \0 就会停止
int len=strlen (s1) ; // 运行,len 为 2
puts (s1); //运行,输出go并换行
归根结底,只有当字符数组在定义的时候赋给他的元素个数少于他的长度,编译器才会自动的在后面添加上 '\0' ,反之在其他地方如果想要数组中拥有 '\0' 结束符,则需要自行添加。
C 中有大量操作字符串的函数:
序号 | 函数 & 目的 |
---|---|
1 | strcpy(s1, s2); 复制字符串 s2 到字符串 s1。 |
2 | strcat(s1, s2); 连接字符串 s2 到字符串 s1 的末尾。 |
3 | strlen(s1); 返回字符串 s1 的长度。 |
4 | strcmp(s1, s2); 如果 s1 和 s2 是相同的,则返回 0;如果 s1<s2 则返回小于 0;如果 s1>s2 则返回大于 0。 |
5 | strchr(s1, ch); 返回一个指针,指向字符串 s1 中字符 ch 的第一次出现的位置。 |
6 | strstr(s1, s2); 返回一个指针,指向字符串 s1 中字符串 s2 的第一次出现的位置。 |
举个栗子:
#include <stdio.h>
#include <string.h>
int main ()
{
char str1[14] = "runoob";
char str2[14] = "google";
char str3[14];
int len ;
/* 复制 str1 到 str3 */
strcpy(str3, str1);
printf("strcpy( str3, str1) : %s\n", str3 );
/* 连接 str1 和 str2 */
strcat( str1, str2);
printf("strcat( str1, str2): %s\n", str1 );
/* 连接后,str1 的总长度 */
len = strlen(str1);
printf("strlen(str1) : %d\n", len );
return 0;
}
0x08 结构体 & 共用体
1、结构体
结构体和共用体的定义方式类似于上面的枚举类型:
struct tag {
member-list
member-list
member-list
...
} variable-list;
/* tag 是结构体标签。
member-list 是标准的变量定义,比如 int i; 或者 float f,或者其他有效的变量定义。
variable-list 结构变量,定义在结构的末尾,最后一个分号之前,您可以指定一个或多个结构变量。下面是声明 Book 结构的方 */
结构体类似于 Java 中的 bean ,举个栗子:
//此声明声明了拥有3个成员的结构体,分别为整型的a,字符型的b和双精度的c
//同时又声明了结构体变量s1
//这个结构体并没有标明其标签
struct
{
int a;
char b;
double c;
} s1;
//此声明声明了拥有3个成员的结构体,分别为整型的a,字符型的b和双精度的c
//结构体的标签被命名为SIMPLE,没有声明变量
struct SIMPLE
{
int a;
char b;
double c;
};
//用SIMPLE标签的结构体,另外声明了变量t1、t2、t3
struct SIMPLE t1, t2[20], *t3;
//也可以用typedef创建新类型
typedef struct
{
int a;
char b;
double c;
} Simple2;
//现在可以用Simple2作为类型声明新的结构体变量
Simple2 u1, u2[20], *u3;
结构体声明中的 tag 和 variable-list 都能够单独的代表一个结构体,也就是说上面代码中的 s1 和SIMPLE 虽然参数列表相同,但是并非同一个结构体,让 SIMPLE 类型的 t3 = &s1 是错误的。
结构体的成员除了基本类型外,还可以是其他的结构体,当然也可以是自己。链表的实现就是借助了结构体这种便利性。
//此结构体的声明包含了其他的结构体
struct COMPLEX
{
char string[100];
struct SIMPLE a;
};
//此结构体的声明包含了指向自己类型的指针
struct NODE
{
char string[100];
struct NODE *next_node;
};
// 如果要两个结构体互相包含,则需要对其中一个结构体进行不完整定义
struct B; //对结构体B进行不完整声明
//结构体A中包含指向结构体B的指针
struct A
{
struct B *partner;
//other members;
};
//结构体B中包含指向结构体A的指针,在A声明完后,B也随之进行声明
struct B
{
struct A *partner;
//other members;
};
访问结构体中的元素:
#include <stdio.h>
#include <string.h>
struct Books
{
char title[50];
char author[50];
char subject[100];
int book_id;
};
int main( )
{
struct Books Book1; /* 声明 Book1,类型为 Books */
struct Books *Book2 = &Book1;
/* Book1 详述 */
strcpy( Book1.title, "C Programming");
strcpy( Book1.author, "Nuha Ali");
strcpy( Book1.subject, "C Programming Tutorial");
Book1.book_id = 6495407;
/* 输出 Book1 信息 */
printf( "Book 1 title : %s\n", Book1.title);
printf( "Book 1 author : %s\n", Book1.author);
printf( "Book 1 subject : %s\n", Book1.subject);
printf( "Book 1 book_id : %d\n", Book1.book_id);
/* 输出 Book2 信息 */
printf( "Book 2 title : %s\n", Book2->title);
printf( "Book 2 author : %s\n", Book2->author);
printf( "Book 2 subject : %s\n", Book2->subject);
printf( "Book 2 book_id : %d\n", Book2->book_id);
return 0;
}
2、共用体
共用体的定义跟结构体类似,不同的是结构体可以同时使用包含在定义的所有类型参数,但是共用体则只能使用一个(因为公用体占用的内存大小原因,具体的可以看下面的例子)。从这点出发就可以知道,结构体占用的内存大小将是所有参数类型大小的总和,而共用体占用的内存大小则是参数中最大的那一个参数类型的大小。
#include <stdio.h>
#include <string.h>
union Data
{
int i;
float f;
char str[20];
};
int main( )
{
union Data data;
data.i = 10;
data.f = 220.5;
strcpy( data.str, "C Programming");
printf( "data.i : %d\n", data.i);
printf( "data.f : %f\n", data.f);
printf( "data.str : %s\n", data.str);
return 0;
}
/*
data.i : 1917853763
data.f : 4122360580327794860452759994368.000000
data.str : C Programming
*/
可以看出,当一次性使用共用体中过多参数时,因为最后赋给变量的值会占用内存,可能导致数据失真问题。所以,同一时间我们只使用共用体的一个参数。
3、位域
4、typedef
typedef 关键字用于给数据类型取别名,他跟 #define 的区别就是 define 不仅可以给数据类型取别名,还可以给常量命名。
// 给常用数据类型取别名
typedef unsigned char BYTE;
BYTE b1, b2;
// 给结构体类型重命名
#include <stdio.h>
#include <string.h>
typedef struct Books
{
char title[50];
char author[50];
char subject[100];
int book_id;
} Book;
int main( )
{
Book book;
strcpy( book.title, "C 教程");
strcpy( book.author, "Runoob");
strcpy( book.subject, "编程语言");
book.book_id = 12345;
printf( "书标题 : %s\n", book.title);
printf( "书作者 : %s\n", book.author);
printf( "书类目 : %s\n", book.subject);
printf( "书 ID : %d\n", book.book_id);
return 0;
}
0x09 文件读写
0x10 强制转换
#include <stdio.h>
int main()
{
int sum = 17, count = 5;
double mean;
mean = (double) sum / count; // 强转的优先级比除法更高
printf("Value of mean : %f\n", mean );
}
编译器在处理不同类型数据的时候会隐式的把值转换为相同的类型(这里一般是较低类型转换为较高类型),但是这种转换只适用于对基本数据类型的操作当中,对于像字符串之类的就没办法进行转换。
0x11 错误打印
C 语言不提供对错误处理的直接支持,但是作为一种系统编程语言,它以返回值的形式允许您访问底层数据。在发生错误时,大多数的 C 或 UNIX 函数调用返回 1 或 NULL,同时会设置一个错误代码 errno,该错误代码是全局变量,表示在函数调用期间发生了错误。您可以在 errno.h 头文件中找到各种各样的错误代码。
所以,C 程序员可以通过检查返回值,然后根据返回值决定采取哪种适当的动作。开发人员应该在程序初始化时,把 errno 设置为 0,这是一种良好的编程习惯。0 值表示程序中没有错误。
C语言提供了函数来打印相关的错误信息
- perror() 函数显示您传给它的字符串,后跟一个冒号、一个空格和当前 errno 值的文本表示形式。
- strerror() 函数,返回一个指针,指针指向当前 errno 值的文本表示形式。
#include <stdio.h>
#include <errno.h>
#include <string.h>
extern int errno ;
int main ()
{
FILE * pf;
int errnum;
pf = fopen ("unexist.txt", "rb");
if (pf == NULL)
{
errnum = errno;
fprintf(stderr, "错误号: %d\n", errno);
perror("通过 perror 输出错误");
fprintf(stderr, "打开文件错误: %s\n", strerror( errnum ));
}
else
{
fclose (pf);
}
return 0;
}
/*
错误号: 2
通过 perror 输出错误: No such file or directory
打开文件错误: No such file or directory
*/
0x12 内存管理
C语言提供了主动管理内存的几个函数。这些函数包含在 <stdlib.h> 中。
序号 | 函数和描述 |
---|---|
1 | void *calloc(int num, int size); 在内存中动态地分配 num 个长度为 size 的连续空间,并将每一个字节都初始化为 0。所以它的结果是分配了 num*size 个字节长度的内存空间,并且每个字节的值都是0。 |
2 | void free(void *address); 该函数释放 address 所指向的内存块,释放的是动态分配的内存空间。 |
3 | void *malloc(int num); 在堆区分配一块指定大小的内存空间,用来存放数据。这块内存空间在函数执行完成后不会被初始化,它们的值是未知的。 |
4 | void *realloc(void *address, int newsize); 该函数重新分配内存,把内存扩展到 newsize。 |
在使用相关函数时通常将返回的无类型指针(*void)强转为需要类型的指针类型( *type)。
具体使用 内存管理
这里需要特别注意的是,再分配的内存使用完毕并且确定后续程序无需再继续使用的前提下,一定要手动的释放掉分配的内存。