备注: Java基础的学习C语言的一些知识点。
基础
1. 进制数
整数常量可以是十进制、八进制或十六进制的常量。
前缀指定基数:0x 或 0X 表示十六进制,0 表示八进制,不带前缀则默认表示十进制。
整数常量也可以带一个后缀,后缀是 U 和 L 的组合,U 表示无符号整数(unsigned),L 表示长整数(long)。后缀可以是大写,也可以是小写,U 和 L 的顺序任意。
2. 浮点数的存储
- float
- double
3. 左值/右值
C 中有两种类型的表达式:
- 左值(lvalue):指向内存位置的表达式被称为左值(lvalue)表达式。左值可以出现在赋值号的左边或右边。
- 右值(rvalue):指的是存储在内存中某些地址的数值。右值是不能对其进行赋值的表达式,也就是说,右值可以出现在赋值号的右边,但不能出现在赋值号的左边。
4. 常量
在 C 中,有两种简单的定义常量的方式:
- 使用 #define 预处理器。
#define LENGTH 10
- 使用 const 关键字。
const int LENGTH = 10;
const关键字
// 从右往左读
// p是一个指针,指向 const char类型
char str[] = "hello";
const char *p = str;
str[0] = 'c'; // 正确
p[0] = 'c'; // 错误 不能通过指针修改 const char
// 可以修改指针指向的数据
// p 原来 指向"hello", 不能修改"hello"中的某个字符,但是可以让p指向新的值。
p = "12345";
// 性质和 const char * 一样
char const *p1;
// p2是一个const指针,指向char类型数据
char * const p2 = str;
p2[0] = 'd'; // 正确
p2 = "12345"; // 错误
// p3是一个const的指针变量, 意味着不能修改它的指向
// 同时指向一个 const char 类型 意味着不能修改它指向的字符
// 集合了 const char * 与 char * const
char const* const p3 = str;
5. 存储类
存储类定义 C 程序中变量/函数的范围(可见性)和生命周期。这些说明符放置在它们所修饰的类型之前。下面列出 C 程序中可用的存储类:
- auto
auto 存储类是所有局部变量默认的存储类。(auto 只能修饰局部变量) - register
register 存储类用于定义存储在寄存器中而不是 RAM 中的局部变量。这意味着变量的最大尺寸等于寄存器的大小(通常是一个词),且不能对它应用一元的 ‘&’ 运算符(因为它没有内存位置)。
寄存器只用于需要快速访问的变量,比如计数器。还应注意的是,定义 ‘register’ 并不意味着变量将被存储在寄存器中,它意味着变量可能存储在寄存器中,这取决于硬件和实现的限制。 - static
static 存储类指示编译器在程序的生命周期内保持局部变量的存在,而不需要在每次它进入和离开作用域时进行创建和销毁。因此,使用 static 修饰局部变量可以在函数调用之间保持局部变量的值。
static 修饰符也可以应用于全局变量。当 static 修饰全局变量时,会使变量的作用域限制在声明它的文件内。
全局声明的一个 static 变量或方法可以被任何函数或方法调用,只要这些方法出现在跟 static 变量或方法同一个文件中。 - extern
extern 存储类用于提供一个全局变量的引用,全局变量对所有的程序文件都是可见的。当您使用 extern 时,对于无法初始化的变量,会把变量名指向一个之前定义过的存储位置。
当您有多个文件且定义了一个可以在其他文件中使用的全局变量或函数时,可以在其他文件中使用 extern 来得到已定义的变量或函数的引用。可以这么理解,extern 是用来在另一个文件中声明一个全局变量或函数。
6. 函数
函数声明
在函数声明中,参数的名称并不重要,只有参数的类型是必需的。
int max(int num1, int num2);
int max(int, int);
函数参数
形式参数就像函数内的其他局部变量,在进入函数时被创建,退出函数时被销毁。
当调用函数时,有两种向函数传递参数的方式:
调用类型 | 描述 |
---|---|
传值调用 | 该方法把参数的实际值复制给函数的形式参数。在这种情况下,修改函数内的形式参数不会影响实际参数。 |
引用调用 | 通过指针传递方式,形参为指向实参地址的指针,当对形参的指向操作时,就相当于对实参本身进行的操作。 |
默认情况下,C 使用传值调用来传递参数。
7. 作用域
当局部变量被定义时,系统不会对其初始化,您必须自行对其初始化。定义全局变量时,系统会自动对其初始化。
全局变量与局部变量在内存中的区别:
全局变量保存在内存的全局存储区中,占用静态的存储单元;
局部变量保存在栈中,只有在所在函数被调用时才动态地为变量分配存储单元。
C/C++ 中 static 的用法全局变量与局部变量
8. 数组
double balance[10];
double balance[5] = {1000.0, 2.0, 3.4, 7.0, 50.0};
double balance[] = {1000.0, 2.0, 3.4, 7.0, 50.0};
int x[3][4];
int a[3][4] = {
{0, 1, 2, 3} , /* 初始化索引号为 0 的行 */
{4, 5, 6, 7} , /* 初始化索引号为 1 的行 */
{8, 9, 10, 11} /* 初始化索引号为 2 的行 */
};
int a[3][4] = {0,1,2,3,4,5,6,7,8,9,10,11};
C 不支持函数返回局部变量的地址,除非定义局部变量为 static 变量。
9. 枚举
enum DAY
{
// 第一个枚举成员的默认值为整型的 0,后续枚举成员的值在前一个成员上加 1。我们在这个实例中把第一个枚举成员的值定义为 1,第二个就为 2,以此类推。
MON=1, TUE, WED, THU, FRI, SAT, SUN
};
// 枚举变量
enum DAY day;
进阶
1. 指针
// NULL指针
int *p = NULL;
指针的算术运算:
- 指针的每一次递增,它其实会指向下一个元素的存储单元。
- 指针的每一次递减,它都会指向前一个元素的存储单元。
- 指针在递增和递减时跳跃的字节数取决于指针所指向变量数据类型长度,比如 int 就是 4 个字节。
函数指针
函数指针是指向函数的指针变量。
通常我们说的指针变量是指向一个整型、字符型或数组等变量,而函数指针是指向函数。
函数指针可以像一般函数一样,用于调用函数、传递参数。
typedef int (*fun_ptr)(int,int); // 声明一个指向同样参数、返回值的函数指针类型
#include <stdio.h>
int max(int x, int y)
{
return x > y ? x : y;
}
int test(int a, int b, int c)
{
/* p 是函数指针 */
// 声明一个指向同样参数、返回值的函数指针类型
int (* p)(int, int) = & max; // &可以省略
/* 与直接调用函数等价,d = max(max(a, b), c) */
int d = p(p(a, b), c);
printf("最大的数字是: %d\n", d);
return d;
}
回调函数
#include <stdio.h>
float max(float a, float b, float c, float (*maxf)(float, float)) {
return maxf(maxf(a, b), c);
}
float max2(float a, float b) {
return a > b ? a : b;
}
int main() {
// 方式一. 函数指针p
float (*p)(float, float) = max2;
float maxValue = max(1.f, 2.f, 3.f, p);
// 方式二. max2 和上面的 函数指针p一样的
float maxValue2 = max(1.f, 2.f, 3.f, max2);
}
2. 字符串
在 C 语言中,字符串实际上是使用 null 字符 \0 终止的一维字符数组。因此,一个以 null 结尾的字符串,包含了组成字符串的字符。
下面的声明和初始化创建了一个 RUNOOB 字符串。由于在数组的末尾存储了空字符,所以字符数组的大小比单词 RUNOOB 的字符数多一个。
char site[7] = {'R', 'U', 'N', 'O', 'O', 'B', '\0'};
char site[] = "RUNOOB";
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 的第一次出现的位置。 |
3. 结构体
结构体的声明
struct Books // 结构体标签
{
char title[50];
} book; // 结构体变量,定义在结构的末尾,最后一个分号之前,您可以指定一个或多个结构变量。
//此声明声明了拥有1个成员的结构体
//同时又声明了结构体变量s1
struct // 省略了结构体标签
{
int a;
} s1;
//此声明声明了拥有1个成员的结构体
//结构体的标签被命名为SIMPLE,没有声明变量
struct SIMPLE
{
int a;
};
//用SIMPLE标签的结构体,另外声明了变量t1、t2、t3
struct SIMPLE t1, t2[20], *t3;
//也可以用typedef创建新类型
typedef struct
{
int a;
} Simple2;
//现在可以用Simple2作为类型声明新的结构体变量
Simple2 u1, u2[20], *u3;
结构体的初始化
// 可以在定义时直接初始化
struct Books
{
char title[50];
} book = {"Hello C"};
访问结构体成员
struct Books
{
char title[50];
char author[50];
char subject[100];
int book_id;
};
int main( )
{
struct Books Book1; /* 声明 Book1,类型为 Books */
/* Book1 详述 */
strcpy( Book1.title, "C Programming");
strcpy( Book1.author, "Nuha Ali");
strcpy( Book1.subject, "C Programming Tutorial");
Book1.book_id = 6495407;
// 指向结构体的指针
struct Books *p = &Book1;
printf("book.title is %s\n", p->title);
return 0;
}
位域
位域在本质上就是一种结构类型。
一个位域存储在同一个字节中,如一个字节所剩空间不够存放另一位域时,则会从下一单元起存放该位域。
struct bs{
unsigned a:4;
unsigned :4; /* 空域 */
unsigned b:4; /* 从下一单元开始存放 */
unsigned c:4;
}data;
4. 共用体
共用体是一种特殊的数据类型,允许您在相同的内存位置存储不同的数据类型。您可以定义一个带有多成员的共用体,但是任何时候只能有一个成员带有值。共用体提供了一种使用相同的内存位置的有效方式。
// 共用体关键字 union
// 共用体占用的内存应足够存储共用体中最大的成员。
// Data 将占用 20 个字节的内存空间,因为在各个成员中,字符串所占用的空间是最大的。
union Data
{
int i;
float f;
char str[20];
} data;
int main( )
{
union Data data;
// 任何时候只能有一个成员带有值
data.i = 10;
printf( "data.i : %d\n", data.i);
// 任何时候只能有一个成员带有值
data.f = 220.5;
printf( "data.f : %f\n", data.f);
// 任何时候只能有一个成员带有值
strcpy( data.str, "C Programming");
printf( "data.str : %s\n", data.str);
return 0;
}
5. 位域
struct
{
// 位域的类型,只能为 int(整型),unsigned int(无符号整型),signed int(有符号整型) 三种类型,决定了如何解释位域的值。
unsigned int widthValidated : 1;
unsigned int heightValidated : 1;
} status;
6. typedef
// 按照惯例,定义时会大写字母,以便提醒用户类型名称是一个象征性的缩写,但您也可以使用小写字母
typedef unsigned char BYTE;
// 对结构体使用 typedef 来定义一个新的数据类型名字,然后使用这个新的数据类型来直接定义结构变量
typedef struct Books
{
char title[50];
char author[50];
char subject[100];
int book_id;
} Book;
Book book;
typedef vs #define
#define 是 C 指令,用于为各种数据类型定义别名,与 typedef 类似,但是它们有以下几点不同:
- typedef 仅限于为类型定义符号名称,#define 不仅可以为类型定义别名,也能为数值定义别名;
- typedef 是由编译器执行解释的,#define 语句是由预编译器进行处理的。
7. 输入输出
C 语言把所有的设备都当作文件。所以设备(比如显示器)被处理的方式与文件相同。以下三个文件会在程序执行时自动打开,以便访问键盘和屏幕。
printf/scanf
C 语言中的 I/O (输入/输出) 通常使用 printf() 和 scanf() 两个函数。
int scanf(const char *format, …) 函数从标准输入流 stdin 读取输入,并根据提供的 format 来浏览输入。
int printf(const char *format, …) 函数把输出写入到标准输出流 stdout ,并根据提供的格式产生输出。
char str[100];
int i;
printf( "Enter a value :");
// 在读取字符串时,只要遇到一个空格,scanf() 就会停止读取,所以 "this is test" 对 scanf() 来说是三个字符串。
scanf("%s %d", str, &i);
printf( "\nYou entered: %s %d ", str, i);
getchar/putchar
int getchar(void) 函数从屏幕读取下一个可用的字符,并把它返回为一个整数。这个函数在同一个时间内只会读取一个单一的字符。您可以在循环内使用这个方法,以便从屏幕上读取多个字符。
int putchar(int c) 函数把字符输出到屏幕上,并返回相同的字符。这个函数在同一个时间内只会输出一个单一的字符。您可以在循环内使用这个方法,以便在屏幕上输出多个字符。
#include <stdio.h>
int main( )
{
int c;
printf( "Enter a value :");
c = getchar( );
printf( "\nYou entered: ");
putchar( c );
printf( "\n");
return 0;
}
gets/puts
char *gets(char *s) 函数从 stdin 读取一行到 s 所指向的缓冲区,直到一个终止符或 EOF。
int puts(const char *s) 函数把字符串 s 和一个尾随的换行符写入到 stdout。
#include <stdio.h>
int main( )
{
char str[100];
printf( "Enter a value :");
gets( str );
printf( "\nYou entered: ");
puts( str );
return 0;
}
高级
1. 文件读写
FILE *fopen( const char * filename, const char * mode );
访问模式 mode 的值可以是下列值中的一个:
模式 | 描述 |
---|---|
r | 打开一个已有的文本文件,允许读取文件。 |
w | 打开一个文本文件,允许写入文件。如果文件不存在,则会创建一个新文件。在这里,您的程序会从文件的开头写入内容。如果文件存在,则该会被截断为零长度,重新写入。 |
a | 打开一个文本文件,以追加模式写入文件。如果文件不存在,则会创建一个新文件。在这里,您的程序会在已有的文件内容中追加内容。 |
r+ | 打开一个文本文件,允许读写文件。 |
w+ | 打开一个文本文件,允许读写文件。如果文件已存在,则文件会被截断为零长度,如果文件不存在,则会创建一个新文件。 |
a+ | 打开一个文本文件,允许读写文件。如果文件不存在,则会创建一个新文件。读取会从文件的开头开始,写入则只能是追加模式。 |
如果处理的是二进制文件,则需使用下面的访问模式来取代上面的访问模式:"rb", "wb", "ab", "rb+", "r+b", "wb+", "w+b", "ab+", "a+b"。
// 如果成功关闭文件,fclose( ) 函数返回零,如果关闭文件时发生错误,函数返回 EOF。
// 这个函数实际上,会清空缓冲区中的数据,关闭文件,并释放用于该文件的所有内存。
// EOF 是一个定义在头文件 stdio.h 中的常量。
int fclose( FILE *fp );
// 函数 fputc() 把参数 c 的字符值写入到 fp 所指向的输出流中。如果写入成功,它会返回写入的字符,如果发生错误,则会返回 EOF。
int fputc( int c, FILE *fp );
// 函数 fputs() 把字符串 s 写入到 fp 所指向的输出流中。如果写入成功,它会返回一个非负值,如果发生错误,则会返回 EOF。
int fputs( const char *s, FILE *fp );
// fgetc() 函数从 fp 所指向的输入文件中读取一个字符。返回值是读取的字符,如果发生错误则返回 EOF。
int fgetc( FILE * fp );
// 函数 fgets() 从 fp 所指向的输入流中读取 n - 1 个字符。它会把读取的字符串复制到缓冲区 buf,并在最后追加一个 null 字符来终止字符串。
// 如果这个函数在读取最后一个字符之前就遇到一个换行符 '\n' 或文件的末尾 EOF,则只会返回读取到的字符,包括换行符。
char *fgets( char *buf, int n, FILE *fp );
// fscanf函数从文件中读取字符串,但是在遇到第一个空格和换行符时,它会停止读取。
int fscanf(FILE *fp, const char *format, ...)
// 下面两个函数用于二进制输入和输出:
// 这两个函数都是用于存储块的读写 - 通常是数组或结构体。
size_t fread(void *ptr, size_t size_of_elements, size_t number_of_elements, FILE *a_file);
size_t fwrite(const void *ptr, size_t size_of_elements, size_t number_of_elements, FILE *a_file);
2. 预处理器
C 预处理器不是编译器的组成部分,但是它是编译过程中一个单独的步骤。简言之,C 预处理器只不过是一个文本替换工具而已,它们会指示编译器在实际编译之前完成所需的预处理。我们将把 C 预处理器(C Preprocessor)简写为 CPP。
预处理指令
所有的预处理器命令都是以井号(#)开头。它必须是第一个非空字符,为了增强可读性,预处理器指令应从第一列开始。下面列出了所有重要的预处理器指令:
指令 | 描述 |
---|---|
#define | 定义宏 |
#include | 包含一个源代码文件 |
#undef | 取消已定义的宏 |
#ifdef | 如果宏已经定义,则返回真 |
#ifndef | 如果宏没有定义,则返回真 |
#if | 如果给定条件为真,则编译下面代码 |
#else | #if 的替代方案 |
#elif | 如果前面的 #if 给定条件不为真,当前条件为真,则编译下面代码 |
#endif | 结束一个 #if……#else 条件编译块 |
#error | 当遇到标准错误时,输出错误消息 |
#pragma | 使用标准化方法,向编译器发布特殊的命令到编译器中 |
预定义宏
ANSI C 定义了许多宏。在编程中您可以使用这些宏,但是不能直接修改这些预定义的宏。
宏 | 描述 |
---|---|
DATE | 当前日期,一个以 “MMM DD YYYY” 格式表示的字符常量。 |
TIME | 当前时间,一个以 “HH:MM:SS” 格式表示的字符常量。 |
FILE | 这会包含当前文件名,一个字符串常量。 |
LINE | 这会包含当前行号,一个十进制常量。 |
STDC | 当编译器以 ANSI 标准编译时,则定义为 1。 |
3. 头文件
A simple practice in C 或 C++ 程序中,建议把所有的常量、宏、系统全局变量和函数原型写在头文件中,在需要的时候随时引用这些头文件。
只引用一次头文件
如果一个头文件被引用两次,编译器会处理两次头文件的内容,这将产生错误。为了防止这种情况,标准的做法是把文件的整个内容放在条件编译语句中,如下:
#ifndef HEADER_FILE
#define HEADER_FILE
the entire header file file
#endif
有条件引用
有时需要从多个不同的头文件中选择一个引用到程序中。例如,需要指定在不同的操作系统上使用的配置参数。
#define SYSTEM_H1 "system_1.h"
#define SYSTEM_H2 "system_2.h"
...
#if SYSTEM_1
# include SYSTEM_H1
#elif SYSTEM_2
# include SYSTEM_H2
#elif SYSTEM_3
...
#endif
4. 错误处理
C 语言不提供对错误处理的直接支持,但是作为一种系统编程语言,它以返回值的形式允许您访问底层数据。在发生错误时,大多数的 C 或 UNIX 函数调用返回 1 或 NULL,同时会设置一个错误代码 errno,该错误代码是全局变量,表示在函数调用期间发生了错误。您可以在 errno.h 头文件中找到各种各样的错误代码。
所以,C 程序员可以通过检查返回值,然后根据返回值决定采取哪种适当的动作。开发人员应该在程序初始化时,把 errno 设置为 0,这是一种良好的编程习惯。0 值表示程序中没有错误。
errno、perror() 和 strerror()
C 语言提供了 perror() 和 strerror() 函数来显示与 errno 相关的文本消息。
- perror() 函数显示您传给它的字符串,后跟一个冒号、一个空格和当前 errno 值的文本表示形式。
- strerror() 函数,返回一个指针,指针指向当前 errno 值的文本表示形式。
5. 可变参数
函数 average() 最后一个参数写成省略号,省略号之前的那个参数是 int,代表了要传递的可变参数的总数。为了使用这个功能,您需要使用 stdarg.h 头文件,该文件提供了实现可变参数功能的函数和宏。
#include <stdio.h>
#include <stdarg.h>
double average(int num,...)
{
/* 定义va_list类型变量valist */
va_list valist;
double sum = 0.0;
int i;
/* 1. 为 num 个参数初始化 valist,宏 va_start 是在 stdarg.h 头文件中定义的 */
va_start(valist, num);
/* 2. 访问所有赋给 valist 的参数 */
for (i = 0; i < num; i++)
{
sum += va_arg(valist, int);
}
/* 3. 清理为 valist 保留的内存 */
va_end(valist);
return sum/num;
}
int main()
{
printf("Average of 2, 3, 4, 5 = %f\n", average(4, 2,3,4,5));
printf("Average of 5, 10, 15 = %f\n", average(3, 5,10,15));
}
6. 内存管理
C 语言为内存的分配和管理提供了几个函数。这些函数可以在 <stdlib.h> 头文件中找到。
序号 | 函数及描述 |
---|---|
1 | void *calloc(int num, int size); 在内存中动态地分配 num 个长度为 size 的连续空间,并将每一个字节都初始化为 0。所以它的结果是分配了 num*size 个字节长度的内存空间,并且每个字节的值都是0。 |
2 | void *malloc(int num); 在堆区分配一块指定大小的内存空间,用来存放数据。这块内存空间在函数执行完成后不会被初始化,它们的值是未知的。 |
3 | void *realloc(void *address, int newsize); 该函数重新分配内存,把内存扩展到 newsize。 |
4 | void free(void *address); 该函数释放 address 所指向的内存块,释放的是动态分配的内存空间。 |
注意:void * 类型表示未确定类型的指针。C、C++ 规定 void * 类型可以通过类型转换强制转换为任何其它类型的指针。
7. 命令行参数
// argc 是指传入参数的个数,argv[] 是一个指针数组,指向传递给程序的每个参数。
int main( int argc, char *argv[] )
{
return 0;
}
备注: argv[0] 存储程序的名称,argv[1] 是一个指向第一个命令行参数的指针,argv[n] 是最后一个参数。如果没有提供任何参数,argc 将为 1,否则,如果传递了一个参数,argc 将被设置为 2。
多个命令行参数之间用空格分隔,但是如果参数本身带有空格,那么传递参数的时候应把参数放置在双引号 “” 或单引号 ‘’ 内部。
练习
线上编译环境 https://www.tutorialspoint.com/compile_c_online.php
C 语言实例
https://www.runoob.com/cprogramming/c-examples.html
C 语言经典100例
https://www.runoob.com/cprogramming/c-100-examples.html
C标准库
https://www.runoob.com/cprogramming/c-standard-library.html
参考:
菜鸟教程-C语言教程