C语言
C语言程序结构
C 程序主要包括以下部分:
- 预处理器指令
- 函数
- 变量
- 语句 & 表达式
- 注释
C语言基本语法
分号 ;
在 C 程序中,分号是语句结束符。也就是说,每个语句必须以分号结束。它表明一个逻辑实体的结束。
例如,下面是两个不同的语句:
printf("Hello, World! \n");
return 0;
注释
C 语言有两种注释方式:
// 单行注释
以 // 开始的单行注释,这种注释可以单独占一行。
/* 单行注释 */
/*
多行注释
多行注释
多行注释
*/
/* */ 这种格式的注释可以单行或多行。
不能在注释内嵌套注释,注释也不能出现在字符串或字符值中。
标识符
C 标识符是用来标识变量、函数,或任何其他用户自定义项目的名称。一个标识符以字母 A-Z 或 a-z 或下划线 _ 开始,后跟零个或多个字母、下划线和数字(0-9)。
C 标识符内不允许出现标点字符,比如 @、$ 和 %。C 是区分大小写的编程语言。因此,在 C 中,Manpower 和 manpower 是两个不同的标识符。下面列出几个有效的标识符:
mohd zara abc move_name a_123
myname50 _temp j a23b9 retVal
关键字
下表列出了 C 中的保留字。这些保留字不能作为常量名、变量名或其他标识符名称。
关键字 | 说明 |
---|---|
auto | 声明自动变量 |
break | 跳出当前循环 |
case | 开关语句分支 |
char | 声明字符型变量或函数返回值类型 |
const | 定义常量,如果一个变量被 const 修饰,那么它的值就不能再被改变 |
continue | 结束当前循环,开始下一轮循环 |
default | 开关语句中的"其它"分支 |
do | 循环语句的循环体 |
double | 声明双精度浮点型变量或函数返回值类型 |
else | 条件语句否定分支(与 if 连用) |
enum | 声明枚举类型 |
extern | 声明变量或函数是在其它文件或本文件的其他位置定义 |
float | 声明浮点型变量或函数返回值类型 |
for | 一种循环语句 |
goto | 无条件跳转语句 |
if | 条件语句 |
int | 声明整型变量或函数 |
long | 声明长整型变量或函数返回值类型 |
register | 声明寄存器变量 |
return | 子程序返回语句(可以带参数,也可不带参数) |
short | 声明短整型变量或函数 |
signed | 声明有符号类型变量或函数 |
sizeof | 计算数据类型或变量长度(即所占字节数) |
static | 声明静态变量 |
struct | 声明结构体类型 |
switch | 用于开关语句 |
typedef | 用以给数据类型取别名 |
unsigned | 声明无符号类型变量或函数 |
union | 声明共用体类型 |
void | 声明函数无返回值或无参数,声明无类型指针 |
volatile | 说明变量在程序执行中可被隐含地改变 |
while | 循环语句的循环条件 |
C99 新增关键字
_Bool | _Complex | _Imaginary | inline | restrict |
---|---|---|---|---|
C11 新增关键字
_Alignas | _Alignof | _Atomic | _Generic | _Noreturn |
---|---|---|---|---|
_Static_assert | _Thread_local |
C 中的空格
只包含空格的行,被称为空白行,可能带有注释,C 编译器会完全忽略它。
在 C 中,空格用于描述空白符、制表符、换行符和注释。空格分隔语句的各个部分,让编译器能识别语句中的某个元素(比如 int)在哪里结束,下一个元素在哪里开始。因此,在下面的语句中:
int age;
在这里,int 和 age 之间必须至少有一个空格字符(通常是一个空白符),这样编译器才能够区分它们。另一方面,在下面的语句中:
fruit = apples + oranges; // 获取水果的总数
fruit 和 =,或者 = 和 apples 之间的空格字符不是必需的,但是为了增强可读性,您可以根据需要适当增加一些空格。
C语言数据类型
在 C 语言中,数据类型指的是用于声明不同类型的变量或函数的一个广泛的系统。变量的类型决定了变量存储占用的空间,以及如何解释存储的位模式。
C 中的类型可分为以下几种:
序号 | 类型与描述 |
---|---|
1 | 基本数据类型 它们是算术类型,包括整型(int)、字符型(char)、浮点型(float)和双精度浮点型(double)。 |
2 | 枚举类型: 它们也是算术类型,被用来定义在程序中只能赋予其一定的离散整数值的变量。 |
3 | void 类型: 类型说明符 void 表示没有值的数据类型,通常用于函数返回值。 |
4 | 派生类型: :包括数组类型、指针类型和结构体类型。 |
数组类型和结构类型统称为聚合类型。函数的类型指的是函数返回值的类型。在本章节接下来的部分我们将介绍基本类型,其他几种类型会在后边几个章节中进行讲解。
整数类型
下表列出了关于标准整数类型的存储大小和值范围的细节:
类型 | 存储大小 | 值范围 |
---|---|---|
char | 1 字节 | -128 到 127 或 0 到 255 |
unsigned char | 1 字节 | 0 到 255 |
signed char | 1 字节 | -128 到 127 |
int | 2 或 4 字节 | -32,768 到 32,767 或 -2,147,483,648 到 2,147,483,647 |
unsigned int | 2 或 4 字节 | 0 到 65,535 或 0 到 4,294,967,295 |
short | 2 字节 | -32,768 到 32,767 |
unsigned short | 2 字节 | 0 到 65,535 |
long | 4 字节 | -2,147,483,648 到 2,147,483,647 |
unsigned long | 4 字节 | 0 到 4,294,967,295 |
注意,各种类型的存储大小与系统位数有关,但目前通用的以64位系统为主。
以下列出了32位系统与64位系统的存储大小的差别(windows 相同):
为了得到某个类型或某个变量在特定平台上的准确大小,您可以使用 sizeof 运算符。表达式 sizeof(type) 得到对象或类型的存储字节大小。下面的实例演示了获取 int 类型的大小:
实例
#include <stdio.h>
#include <limits.h>
int main()
{
printf("int 存储大小 : %lu \n", sizeof(int));
return 0;
}
%lu 为 32 位无符号整数,详细说明查看 C 库函数 - printf()。
当您在 Linux 上编译并执行上面的程序时,它会产生下列结果:
int 存储大小 : 4
浮点类型
下表列出了关于标准浮点类型的存储大小、值范围和精度的细节:
类型 | 存储大小 | 值范围 | 精度 |
---|---|---|---|
float | 4 字节 | 1.2E-38 到 3.4E+38 | 6 位有效位 |
double | 8 字节 | 2.3E-308 到 1.7E+308 | 15 位有效位 |
long double | 16 字节 | 3.4E-4932 到 1.1E+4932 | 19 位有效位 |
头文件 float.h 定义了宏,在程序中可以使用这些值和其他有关实数二进制表示的细节。下面的实例将输出浮点类型占用的存储空间以及它的范围值:
实例
#include <stdio.h>
#include <float.h>
int main()
{
printf("float 存储最大字节数 : %lu \n", sizeof(float));
printf("float 最小值: %E\n", FLT_MIN );
printf("float 最大值: %E\n", FLT_MAX );
printf("精度值: %d\n", FLT_DIG );
return 0;
}
%E 为以指数形式输出单、双精度实数,详细说明查看 C 库函数 - printf()。
当在 Linux 上编译并执行上面的程序时,它会产生下列结果:
float 存储最大字节数 : 4
float 最小值: 1.175494E-38
float 最大值: 3.402823E+38
精度值: 6
void 类型
void 类型指定没有可用的值。通常用于以下三种情况下:
序号 | 类型与描述 |
---|---|
1 | 函数返回为空 C 中有各种函数都不返回值,或者您可以说它们返回空。不返回值的函数的返回类型为空。例如 void exit (int status); |
2 | 函数参数为空 C 中有各种函数不接受任何参数。不带参数的函数可以接受一个 void。例如 int rand(void); |
3 | 指针指向 void 类型为 void * 的指针代表对象的地址,而不是类型。例如,内存分配函数 void *malloc( size_t size ); 返回指向 void 的指针,可以转换为任何数据类型。 |
类型转换
类型转换是将一个数据类型的值转换为另一种数据类型的值。
C 语言中有两种类型转换:
- **隐式类型转换:**隐式类型转换是在表达式中自动发生的,无需进行任何明确的指令或函数调用。它通常是将一种较小的类型自动转换为较大的类型,例如,将int类型转换为long类型或float类型转换为double类型。隐式类型转换也可能会导致数据精度丢失或数据截断。
- **显式类型转换:**显式类型转换需要使用强制类型转换运算符(type casting operator),它可以将一个数据类型的值强制转换为另一种数据类型的值。强制类型转换可以使程序员在必要时对数据类型进行更精确的控制,但也可能会导致数据丢失或截断。
隐式类型转换实例:
##示例
int i = 10;
float f = 3.14;
double d = i + f; *// 隐式将int类型转换为double类型*
显式类型转换实例:
## 示例
double d = 3.14159;
int i = (int)d; *// 显式将double类型转换为int类型*
C 变量
变量其实只不过是程序可操作的存储区的名称。C 中每个变量都有特定的类型,类型决定了变量存储的大小和布局,该范围内的值都可以存储在内存中,运算符可应用于变量上。
变量的名称可以由字母、数字和下划线字符组成。它必须以字母或下划线开头。大写字母和小写字母是不同的,因为 C 是大小写敏感的。
以下几种基本的变量类型:
类型 | 描述 |
---|---|
char | 通常是一个字节(八位), 这是一个整数类型。 |
int | 整型,4 个字节,取值范围 -2147483648 到 2147483647。 |
float | 单精度浮点值。单精度是这样的格式,1位符号,8位指数,23位小数。 |
double | 双精度浮点值。双精度是1位符号,11位指数,52位小数。 |
void | 表示类型的缺失。 |
C 语言也允许定义各种其他类型的变量,比如枚举、指针、数组、结构、共用体等等,这将会在后续的章节中进行讲解,本章节我们先讲解基本变量类型。
全局变量和局部变量的区别
-
作用域:全局变量是在函数外部定义的,它们在整个程序中都是可见的,这意味着任何函数都可以访问和修改全局变量的值。相反,局部变量是在函数内部定义的,它们只能在该函数内部被访问和修改。
-
生命周期:全局变量的生命周期从它们被定义的那一刻开始,直到程序结束。而局部变量的生命周期则局限于定义它们的函数;当函数执行完毕时,局部变量就会被销毁。
-
使用方式:如果在函数内部需要修改全局变量的值,通常需要使用关键字
global
来声明。而局部变量的使用不需要特别的声明,直接在函数内部定义即可使用。 -
当全局变量过多时;可能会造成内存泄露问题
+++
C 中的变量定义
变量定义就是告诉编译器在何处创建变量的存储,以及如何创建变量的存储。变量定义指定一个数据类型,并包含了该类型的一个或多个变量的列表,如下所示:
type variable_list;
type 表示变量的数据类型,可以是整型、浮点型、字符型、指针等,也可以是用户自定义的对象。
variable_list 可以由一个或多个变量的名称组成,多个变量之间用逗号**,**分隔,变量由字母、数字和下划线组成,且以字母或下划线开头。
下面列出几个有效的声明:
定义整型变量:
int age;
以上代码中,age 被定义为一个整型变量。
定义浮点型变量:
float salary;
以上代码中,salary 被定义为一个浮点型变量。
定义字符型变量:
char grade;
以上代码中,grade 被定义为一个字符型变量。
定义指针变量:
int *ptr;
以上代码中,ptr 被定义为一个整型指针变量。
定义多个变量:
int i, j, k;
int i, j, k; 声明并定义了变量 i、j 和 k,这指示编译器创建类型为 int 的名为 i、j、k 的变量。
变量初始化
在 C 语言中,变量的初始化是在定义变量的同时为其赋予一个初始值。变量的初始化可以在定义时进行,也可以在后续的代码中进行。
初始化器由一个等号,后跟一个常量表达式组成,如下所示:
type variable_name = value;
其中,type 表示变量的数据类型,variable_name 是变量的名称,value 是变量的初始值。
下面列举几个实例:
int x = 10; // 整型变量 x 初始化为 10
float pi = 3.14; // 浮点型变量 pi 初始化为 3.14
char ch = 'A'; // 字符型变量 ch 初始化为字符 'A'
extern int d = 3, f = 5; // d 和 f 的声明与初始化
int d = 3, f = 5; // 定义并初始化 d 和 f
byte z = 22; // 定义并初始化 z
后续初始化变量:
在变量定义后的代码中,可以使用赋值运算符 = 为变量赋予一个新的值。
type variable_name; // 变量定义
variable_name = new_value; // 变量初始化
实例如下:
int x; // 整型变量x定义
x = 20; // 变量x初始化为20
float pi; // 浮点型变量pi定义
pi = 3.14159; // 变量pi初始化为3.14159
char ch; // 字符型变量ch定义
ch = 'B'; // 变量ch初始化为字符'B'
需要注意的是,变量在使用之前应该被初始化。未初始化的变量的值是未定义的,可能包含任意的垃圾值。因此,为了避免不确定的行为和错误,建议在使用变量之前进行初始化。
变量不初始化
在 C 语言中,如果变量没有显式初始化,那么它的默认值将取决于该变量的类型和其所在的作用域。
对于全局变量和静态变量(在函数内部定义的静态变量和在函数外部定义的全局变量),它们的默认初始值为零。
以下是不同类型的变量在没有显式初始化时的默认值:
- 整型变量(int、short、long等):默认值为0。
- 浮点型变量(float、double等):默认值为0.0。
- 字符型变量(char):默认值为’\0’,即空字符。
- 指针变量:默认值为NULL,表示指针不指向任何有效的内存地址。
- 数组、结构体、联合等复合类型的变量:它们的元素或成员将按照相应的规则进行默认初始化,这可能包括对元素递归应用默认规则。
需要注意的是,局部变量(在函数内部定义的非静态变量)不会自动初始化为默认值,它们的初始值是未定义的(包含垃圾值)。因此,在使用局部变量之前,应该显式地为其赋予一个初始值。
总结起来,C 语言中变量的默认值取决于其类型和作用域。全局变量和静态变量的默认值为 0,字符型变量的默认值为 \0,指针变量的默认值为 NULL,而局部变量没有默认值,其初始值是未定义的。
C 中的变量声明
变量声明向编译器保证变量以指定的类型和名称存在,这样编译器在不需要知道变量完整细节的情况下也能继续进一步的编译。变量声明只在编译时有它的意义,在程序连接时编译器需要实际的变量声明。
变量的声明有两种情况:
- 1、一种是需要建立存储空间的。例如:int a 在声明的时候就已经建立了存储空间。
- 2、另一种是不需要建立存储空间的,通过使用extern关键字声明变量名而不定义它。 例如:extern int a 其中变量 a 可以在别的文件中定义的。
- 除非有extern关键字,否则都是变量的定义。
extern int i; //声明,不是定义
int i; //声明,也是定义
实例
尝试下面的实例,其中,变量在头部就已经被声明,但是定义与初始化在主函数内:
#include <stdio.h>
// 函数外定义变量 x 和 y
int x;
int y;
int addtwonum()
{
// 函数内声明变量 x 和 y 为外部变量
extern int x;
extern int y;
// 给外部变量(全局变量)x 和 y 赋值
x = 1;
y = 2;
return x+y;
}
int main()
{
int result;
// 调用函数 addtwonum
result = addtwonum();
printf("result 为: %d",result);
return 0;
}
当上面的代码被编译和执行时,它会产生下列结果:
result 为: 3
如果需要在一个源文件中引用另外一个源文件中定义的变量,我们只需在引用的文件中将变量加上 extern 关键字的声明即可。
+++
C 中的左值(Lvalues)和右值(Rvalues)
C 中有两种类型的表达式:
- **左值(lvalue):**指向内存位置的表达式被称为左值(lvalue)表达式。左值可以出现在赋值号的左边或右边。
- **右值(rvalue):**术语右值(rvalue)指的是存储在内存中某些地址的数值。右值是不能对其进行赋值的表达式,也就是说,右值可以出现在赋值号的右边,但不能出现在赋值号的左边。
变量是左值,因此可以出现在赋值号的左边。数值型的字面值是右值,因此不能被赋值,不能出现在赋值号的左边。下面是一个有效的语句:
int g = 20;
但是下面这个就不是一个有效的语句,会生成编译时错误:
10 = 20;
C 常量
常量是固定值,在程序执行期间不会改变。这些固定的值,又叫做字面量。
常量可以是任何的基本数据类型,比如整数常量、浮点常量、字符常量,或字符串字面值,也有枚举常量。
常量就像是常规的变量,只不过常量的值在定义后不能进行修改。
常量可以直接在代码中使用,也可以通过定义常量来使用。
整数常量
整数常量可以是十进制、八进制或十六进制的常量。前缀指定基数:0x 或 0X 表示十六进制,0 表示八进制,不带前缀则默认表示十进制。
整数常量也可以带一个后缀,后缀是 U 和 L 的组合,U 表示无符号整数(unsigned),L 表示长整数(long)。后缀可以是大写,也可以是小写,U 和 L 的顺序任意。
下面列举几个整数常量的实例:
212 /* 合法的 */
215u /* 合法的 */
0xFeeL /* 合法的 */
078 /* 非法的:8 不是八进制的数字 */
032UU /* 非法的:不能重复后缀 */
以下是各种类型的整数常量的实例:
85 /* 十进制 */
0213 /* 八进制 */
0x4b /* 十六进制 */
30 /* 整数 */
30u /* 无符号整数 */
30l /* 长整数 */
30ul /* 无符号长整数 */
整数常量可以带有一个后缀表示数据类型,例如:
int myInt = 10;
long myLong = 100000L;
unsigned int myUnsignedInt = 10U;
浮点常量
浮点常量由整数部分、小数点、小数部分和指数部分组成。您可以使用小数形式或者指数形式来表示浮点常量。
当使用小数形式表示时,必须包含整数部分、小数部分,或同时包含两者。当使用指数形式表示时, 必须包含小数点、指数,或同时包含两者。带符号的指数是用 e 或 E 引入的。
下面列举几个浮点常量的实例:
3.14159 /* 合法的 */
314159E-5L /* 合法的 */
510E /* 非法的:不完整的指数 */
210f /* 非法的:没有小数或指数 */
.e55 /* 非法的:缺少整数或分数 */
浮点数常量可以带有一个后缀表示数据类型,例如:
float myFloat = 3.14f;
double myDouble = 3.14159;
字符常量
字符常量是括在单引号中,例如,‘x’ 可以存储在 char 类型的简单变量中。
字符常量可以是一个普通的字符(例如 ‘x’)、一个转义序列(例如 ‘\t’),或一个通用的字符(例如 ‘\u02C0’)。
在 C 中,有一些特定的字符,当它们前面有反斜杠时,它们就具有特殊的含义,被用来表示如换行符(\n)或制表符(\t)等。下表列出了一些这样的转义序列码:
转义序列 | 含义 |
---|---|
\ | \ 字符 |
’ | ’ 字符 |
" | " 字符 |
? | ? 字符 |
\a | 警报铃声 |
\b | 退格键 |
\f | 换页符 |
\n | 换行符 |
\r | 回车 |
\t | 水平制表符 |
\v | 垂直制表符 |
\ooo | 一到三位的八进制数 |
\xhh . . . | 一个或多个数字的十六进制数 |
下面的实例显示了一些转义序列字符:
#include <stdio.h>
int main()
{
printf("Hello\tWorld\n\n");
return 0;
}
当上面的代码被编译和执行时,它会产生下列结果:
Hello World
字符常量的 ASCII 值可以通过强制类型转换转换为整数值。
char myChar = 'a';
int myAsciiValue = (int) myChar; // 将 myChar 转换为 ASCII 值 97
字符串常量
字符串字面值或常量是括在双引号 " " 中的。一个字符串包含类似于字符常量的字符:普通的字符、转义序列和通用的字符。
您可以使用空格做分隔符,把一个很长的字符串常量进行分行。
下面的实例显示了一些字符串常量。下面这三种形式所显示的字符串是相同的。
"hello, dear"
"hello, \
dear"
"hello, " "d" "ear"
字符串常量在内存中以 null 终止符 \0 结尾。例如:
char myString[] = "Hello, world!"; //系统对字符串常量自动加一个 '\0'
定义常量
在 C 中,有两种简单的定义常量的方式:
- 使用 #define 预处理器: #define 可以在程序中定义一个常量,它在编译时会被替换为其对应的值。
- 使用 const 关键字:const 关键字用于声明一个只读变量,即该变量的值不能在程序运行时修改。
#define 预处理器
下面是使用 #define 预处理器定义常量的形式:
#define 常量名 常量值
下面的代码定义了一个名为 PI 的常量:
#define PI 3.14159
在程序中使用该常量时,编译器会将所有的 PI 替换为 3.14159。
具体请看下面的实例:
#include <stdio.h>
#define LENGTH 10
#define WIDTH 5
#define NEWLINE '\n'
int main()
{
int area;
area = LENGTH * WIDTH;
printf("value of area : %d", area);
printf("%c", NEWLINE);
return 0;
}
当上面的代码被编译和执行时,它会产生下列结果:
value of area : 50
const 关键字
您可以使用 const 前缀声明指定类型的常量,如下所示:
const 数据类型 常量名 = 常量值;
下面的代码定义了一个名为MAX_VALUE的常量:
const int MAX_VALUE = 100;
在程序中使用该常量时,其值将始终为100,并且不能被修改。
const 声明常量要在一个语句内完成:
具体请看下面的实例:
#include <stdio.h>
int main()
{
const int LENGTH = 10;
const int WIDTH = 5;
const char NEWLINE = '\n';
int area;
area = LENGTH * WIDTH;
printf("value of area : %d", area);
printf("%c", NEWLINE);
return 0;
}
当上面的代码被编译和执行时,它会产生下列结果:
value of area : 50
请注意,把常量定义为大写字母形式,是一个很好的编程习惯。
#define 与 const 区别
#define 与 const 这两种方式都可以用来定义常量,选择哪种方式取决于具体的需求和编程习惯。通常情况下,建议使用 const 关键字来定义常量,因为它具有类型检查和作用域的优势,而 #define 仅进行简单的文本替换,可能会导致一些意外的问题。
#define 预处理指令和 const 关键字在定义常量时有一些区别:
- 替换机制:
#define
是进行简单的文本替换,而const
是声明一个具有类型的常量。#define
定义的常量在编译时会被直接替换为其对应的值,而const
定义的常量在程序运行时会分配内存,并且具有类型信息。 - 类型检查:
#define
不进行类型检查,因为它只是进行简单的文本替换。而const
定义的常量具有类型信息,编译器可以对其进行类型检查。这可以帮助捕获一些潜在的类型错误。 - 作用域:
#define
定义的常量没有作用域限制,它在定义之后的整个代码中都有效。而const
定义的常量具有块级作用域,只在其定义所在的作用域内有效。 - 调试和符号表:使用
#define
定义的常量在符号表中不会有相应的条目,因为它只是进行文本替换。而使用const
定义的常量会在符号表中有相应的条目,有助于调试和可读性。
C 运算符
运算符是一种告诉编译器执行特定的数学或逻辑操作的符号。C 语言内置了丰富的运算符,并提供了以下类型的运算符:
- 算术运算符
- 关系运算符
- 逻辑运算符
- 位运算符
- 赋值运算符
- 杂项运算符
本章将逐一介绍算术运算符、关系运算符、逻辑运算符、位运算符、赋值运算符和其他运算符。
算术运算符
下表显示了 C 语言支持的所有算术运算符。假设变量 A 的值为 10,变量 B 的值为 20,则:
运算符 | 描述 | 实例 |
---|---|---|
+ | 把两个操作数相加 | A + B 将得到 30 |
- | 从第一个操作数中减去第二个操作数 | A - B 将得到 -10 |
* | 把两个操作数相乘 | A * B 将得到 200 |
/ | 分子除以分母 | B / A 将得到 2 |
% | 取模运算符,整除后的余数 | B % A 将得到 0 |
++ | 自增运算符,整数值增加 1 | A++ 将得到 11 |
– | 自减运算符,整数值减少 1 | A-- 将得到 9 |
实例,C 语言中所有可用的算术运算符:
#include <stdio.h>
int main()
{
int a = 21;
int b = 10;
int c ;
c = a + b;
printf("Line 1 - c 的值是 %d\n", c );
c = a - b;
printf("Line 2 - c 的值是 %d\n", c );
c = a * b;
printf("Line 3 - c 的值是 %d\n", c );
c = a / b;
printf("Line 4 - c 的值是 %d\n", c );
c = a % b;
printf("Line 5 - c 的值是 %d\n", c );
c = a++; // 赋值后再加 1 ,c 为 21,a 为 22
printf("Line 6 - c 的值是 %d\n", c );
c = a--; // 赋值后再减 1 ,c 为 22 ,a 为 21
printf("Line 7 - c 的值是 %d\n", c );
}
当上面的代码被编译和执行时,它会产生下列结果:
Line 1 - c 的值是 31
Line 2 - c 的值是 11
Line 3 - c 的值是 210
Line 4 - c 的值是 2
Line 5 - c 的值是 1
Line 6 - c 的值是 21
Line 7 - c 的值是 22
隐式转换和强制转换
C语言中的类型转换分为隐式转换和强制转换两种。
- 隐式转换(Implicit Conversion):
隐式转换是由编译器自动完成的类型转换,无需程序员显式地进行操作。隐式转换通常发生在不同类型的数据进行运算时,编译器会根据一定的规则将一种类型的数据自动转换为另一种类型的数据。以下是一些常见的隐式转换规则:
- 当整数和浮点数进行运算时,整数会自动转换为浮点数;
- 当两个不同类型的整数进行运算时,较小的类型会自动转换为较大的类型;
- 当有符号整数和无符号整数进行运算时,有符号整数会自动转换为无符号整数;
- 当赋值语句中,右侧表达式的类型与左侧变量的类型不匹配时,右侧表达式的类型会自动转换为左侧变量的类型。
- 强制转换(Explicit Conversion):
强制转换是程序员显式地进行类型转换,需要使用类型转换运算符。强制转换可以覆盖隐式转换的规则,使数据按照程序员指定的方式进行转换。强制转换的语法格式为:
(目标类型)(表达式)
例如,将一个浮点数强制转换为整数:
int a = (int)3.14;
a=(int)(3.14+3);
需要注意的是,强制转换可能会导致数据精度的损失或者溢出等问题,因此在使用强制转换时要谨慎。
自增自减运算符的注意事项
a++ 与 ++a 的区别
关系运算符
下表显示了 C 语言支持的所有关系运算符。假设变量 A 的值为 10,变量 B 的值为 20,则:
运算符 | 描述 | 实例 |
---|---|---|
== | 检查两个操作数的值是否相等,如果相等则条件为真。 | (A == B) 为假。 |
!= | 检查两个操作数的值是否相等,如果不相等则条件为真。 | (A != B) 为真。 |
> | 检查左操作数的值是否大于右操作数的值,如果是则条件为真。 | (A > B) 为假。 |
< | 检查左操作数的值是否小于右操作数的值,如果是则条件为真。 | (A < B) 为真。 |
>= | 检查左操作数的值是否大于或等于右操作数的值,如果是则条件为真。 | (A >= B) 为假。 |
<= | 检查左操作数的值是否小于或等于右操作数的值,如果是则条件为真。 | (A <= B) 为真。 |
逻辑运算符
下表显示了 C 语言支持的所有关系逻辑运算符。假设变量 A 的值为 1,变量 B 的值为 0,则:
运算符 | 描述 | 实例 |
---|---|---|
&& | 称为逻辑与运算符。如果两个操作数都非零,则条件为真。 | (A && B) 为假。 |
|| | 称为逻辑或运算符。如果两个操作数中有任意一个非零,则条件为真。 | (A || B) 为真。 |
! | 称为逻辑非运算符。用来逆转操作数的逻辑状态。如果条件为真则逻辑非运算符将使其为假。 | !(A && B) 为真。 |
原码;反码;补码;
在计算机中,负数以其正值的补码形式表达
什么叫补码呢?这得从原码,反码说起。
**原码:**一个整数,按照绝对值大小转换成的二进制数,称为原码。
比如 00000000 00000000 00000000 00000101 是 5 的原码。
**反码:**将二进制数按位取反,所得的新二进制数称为原二进制数的反码。
取反操作指:原为 1,得 0;原为 0,得 1。(1 变 0; 0 变 1)
比如:将 00000000 00000000 00000000 00000101 每一位取反,得 11111111 11111111 11111111 11111010。
称:11111111 11111111 11111111 11111010是 00000000 00000000 00000000 00000101 的反码。
反码是相互的,所以也可称:
11111111 11111111 11111111 11111010 和00000000 00000000 00000000 00000101 互为反码。
**补码:**反码加1称为补码。
也就是说,要得到一个数的补码,先得到反码,然后将反码加上 1,所得数称为补码。
比如:00000000 00000000 00000000 00000101 的反码是:11111111 11111111 11111111 11111010。
那么,补码为:
11111111 11111111 11111111 11111010 + 1 = 11111111 11111111 11111111 11111011
所以,-5 在计算机中表达为:11111111 11111111 11111111 11111011。
位运算符
位运算符作用于位,并逐位执行操作。&、 | 和 ^ 的真值表如下所示:
p | q | p & q | p | q | p ^ q |
---|---|---|---|---|
0 | 0 | 0 | 0 | 0 |
0 | 1 | 0 | 1 | 1 |
1 | 1 | 1 | 1 | 0 |
1 | 0 | 0 | 1 | 1 |
下表显示了 C 语言支持的位运算符。假设变量 A 的值为 60,变量 B 的值为 13,则:
运算符 | 描述 | 实例 |
---|---|---|
& | 对两个操作数的每一位执行逻辑与操作,如果两个相应的位都为 1,则结果为 1,否则为 0。按位与操作,按二进制位进行"与"运算。运算规则:0&0=0; 0&1=0; 1&0=0; 1&1=1; | (A & B) 将得到 12,即为 0000 1100 |
| | 对两个操作数的每一位执行逻辑或操作,如果两个相应的位都为 0,则结果为 0,否则为 1。按位或运算符,按二进制位进行"或"运算。运算规则:`0 | 0=0; 0 |
^ | 对两个操作数的每一位执行逻辑异或操作,如果两个相应的位值相同,则结果为 0,否则为 1。异或运算符,按二进制位进行"异或"运算。运算规则:0^0=0; 0^1=1; 1^0=1; 1^1=0; | (A ^ B) 将得到 49,即为 0011 0001 |
~ | 对操作数的每一位执行逻辑取反操作,即将每一位的 0 变为 1,1 变为 0。取反运算符,按二进制位进行"取反"运算。运算规则:~1=-2; ~0=-1; | (~A ) 将得到 -61,即为 1100 0011,一个有符号二进制数的补码形式。 |
<< | 将操作数的所有位向左移动指定的位数。左移 n 位相当于乘以 2 的 n 次方。二进制左移运算符。将一个运算对象的各二进制位全部左移若干位(左边的二进制位丢弃,右边补0)。 | A << 2 将得到 240,即为 1111 0000 |
>> | 将操作数的所有位向右移动指定的位数。右移n位相当于除以 2 的 n 次方。二进制右移运算符。将一个数的各二进制位全部右移若干位,正数左补 0,负数左补 1,右边丢弃。 | A >> 2 将得到 15,即为 0000 1111 |
+++
有符号数左移右移:
赋值运算符
下表列出了 C 语言支持的赋值运算符:
运算符 | 描述 | 实例 |
---|---|---|
= | 简单的赋值运算符,把右边操作数的值赋给左边操作数 | C = A + B 将把 A + B 的值赋给 C |
+= | 加且赋值运算符,把右边操作数加上左边操作数的结果赋值给左边操作数 | C += A 相当于 C = C + A |
-= | 减且赋值运算符,把左边操作数减去右边操作数的结果赋值给左边操作数 | C -= A 相当于 C = C - A |
*= | 乘且赋值运算符,把右边操作数乘以左边操作数的结果赋值给左边操作数 | C *= A 相当于 C = C * A |
/= | 除且赋值运算符,把左边操作数除以右边操作数的结果赋值给左边操作数 | C /= A 相当于 C = C / A |
%= | 求模且赋值运算符,求两个操作数的模赋值给左边操作数 | C %= A 相当于 C = C % A |
<<= | 左移且赋值运算符 | C <<= 2 等同于 C = C << 2 |
>>= | 右移且赋值运算符 | C >>= 2 等同于 C = C >> 2 |
&= | 按位与且赋值运算符 | C &= 2 等同于 C = C & 2 |
^= | 按位异或且赋值运算符 | C ^= 2 等同于 C = C ^ 2 |
|= | 按位或且赋值运算符 | C |= 2 等同于 C = C | 2 |
杂项运算符 ↦ sizeof & 三元
下表列出了 C 语言支持的其他一些重要的运算符,包括 sizeof 和 ? :。
运算符 | 描述 | 实例 |
---|---|---|
sizeof() | 返回变量的大小。 | sizeof(a) 将返回 4,其中 a 是整数。 |
& | 返回变量的地址。 | &a; 将给出变量的实际地址。 |
* | 指向一个变量。 | *a; 将指向一个变量。 |
? : | 条件表达式 | 如果条件为真 ? 则值为 X : 否则值为 Y |
C 中的运算符优先级
运算符的优先级确定表达式中项的组合。这会影响到一个表达式如何计算。某些运算符比其他运算符有更高的优先级,例如,乘除运算符具有比加减运算符更高的优先级。
例如 x = 7 + 3 * 2,在这里,x 被赋值为 13,而不是 20,因为运算符 * 具有比 + 更高的优先级,所以首先计算乘法 3*2,然后再加上 7。
下表将按运算符优先级从高到低列出各个运算符,具有较高优先级的运算符出现在表格的上面,具有较低优先级的运算符出现在表格的下面。在表达式中,较高优先级的运算符会优先被计算。
类别 | 运算符 | 结合性 |
---|---|---|
后缀 | () [] -> . ++ - - | 从左到右 |
一元 | + - ! ~ ++ - - (type)* & sizeof | 从右到左 |
乘除 | * / % | 从左到右 |
加减 | + - | 从左到右 |
移位 | << >> | 从左到右 |
关系 | < <= > >= | 从左到右 |
相等 | == != | 从左到右 |
位与 AND | & | 从左到右 |
位异或 XOR | ^ | 从左到右 |
位或 OR | | | 从左到右 |
逻辑与 AND | && | 从左到右 |
逻辑或 OR | || | 从左到右 |
条件 | ?: | 从右到左 |
赋值 | = += -= *= /= %=>>= <<= &= ^= |= | 从右到左 |
逗号 | , | 从左到右 |
逗号表达式
格式;表达式1,表达式2,表达式3,… ,表达式n
逗号表达式的要领:
(1) 逗号表达式的运算过程为:从左往右逐个计算表达式。
(2) 逗号表达式作为一个整体,它的值为最后一个表达式(也即表达式n)的值。
(3) 逗号运算符的优先级别在所有运算符中最低。
题目:以下程序的输出结果是:
main()
{
int x,y,z;
x=1;
y=1;
z=x++,y++,++y;
printf(“%d,%d,%d\n”,x,y,z);
}
[A]2,3,3 [B]2,3,2 [C]2,3,1 [D]1,1,1
解析:
x和y的值经过自增以后分别为2和3,D可以排除。剩下3个选项选择什么呢?
如果是(x++,y++,++y)实际上可以看成(1,1,3)整个逗号表达式的值应该是3,那么选A。
如果是(x++,++y,y++)实际上可以看成(1,2,2)整个逗号表达式的值应该是2,那么选B。
但这是错的,这儿还有赋值运算符。赋值运算符的优先级是14,而逗号表达式的优先级是15,也就是说上面的表达式中应该等价于这样的结合:(z=x++),y++,++y;如果这样写的话,则答案很清晰,为:2,3,1
正确答案选C。
例题1:(a = 3,b = 5,b+ = a,c = b* 5),求逗号表达式的值?
答案:40。前两个表达式只是赋值,从第三个开始计算,b+=a,即b=b+a,即b=5+3,b=8,求最后一个表达式,c=b×5=8×5=40.因为逗号表达式的值是最后一个表达式的值,所以整个逗号表达式的值为40,其他各变量最后的值依次为:a=3,b=8,c=40
例题2:若已定义x和y为double类型,则表达式:x=1,y=x+3/2的值是 A) 1 B) 2 C) 2.0 D) 2.5
分析:该表达式是一个逗号表达式,所以先运算x=1,结果变量x中的值为1.0,然后运算y=x+3/2,其结果是变量y中的值为2.0(这个运算过程可参阅本专题的“整数除法的注意事项”——整数相除,舍入法取整数部分),注意此时表达式y=x+3/2的值即等于变量y的值为2.0。最后,整个逗号表达式的值应该等于最后一个表达式的值2.0,所以,正确答案是C)。
分支
判断语句
C 语言提供了以下类型的判断语句。点击链接查看每个语句的细节。
语句 | 描述 |
---|---|
if 语句 | 一个 if 语句 由一个布尔表达式后跟一个或多个语句组成。 |
if…else 语句 | 一个 if 语句 后可跟一个可选的 else 语句,else 语句在布尔表达式为假时执行。 |
嵌套 if 语句 | 您可以在一个 if 或 else if 语句内使用另一个 if 或 else if 语句。 |
switch 语句 | 一个 switch 语句允许测试一个变量等于多个值时的情况。 |
嵌套 switch 语句 | 您可以在一个 switch 语句内使用另一个 switch 语句。 |
//将变量a的第2位设置为1,其他位保持不变
uint8_t a = 0b10110011; // 0xb3;
a = 0b10110111;
a |= 0b00000100;
//将变量b的第2位、第6位设置为1,其他位保持不变
uint8_t b = 0b10110011; // 0xb3;
b |= 0b01000100;
//将变量c的第5位设置为0,其他位保持不变
uint8_t c = 0b10110011; // 0xb3;
c &= 0b11011111;
//将变量d的第0~3位设置为0,其他位保持不变
uint8_t d = 0b11111111; // 0xff;
d = 0b11110000;
//将变量e的第2位取反,其他位保持不变
uint8_t e = 0b10110011; // 0xb3;
e ^= 0b00000100;
//将变量f取出8-15位
uint32_t f = 0x12345678;
uint32_t l = 0;
l=f&0xff00;
int score;
printf("请输入成绩:");
scanf_s("%d", &score);
if (score > 90) {
printf("优秀\n");
}
else if (score > 80) {
printf("中\n");
}
else if (score > 70) {
printf("良\n");
}
else if (score > 60) {
printf("及格\n");
}
else {
printf("不及格\n");
}
int year;
printf("请输入一个年份:\n");
scanf_s("%d", &year);
if ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)) {
printf("%d年是闰年\n", year);
}
else {
printf("%d年是平年\n", year);
}
int month;
printf("输入年份:\n");
scanf_s("%d", &year);
printf("输入月份:\n");
scanf_s("%d", &month);
int days;
if (month == 1 || month == 3 || month == 5 || month == 7 || month == 8 || month == 10 || month == 12) {
days = 31;
}
else if (month == 4 || month == 6 || month == 9 || month == 11) {
days = 30;
}
else if (month == 2) {
if ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)) {
days = 29;
}
else {
days = 28;
}
}
else {
printf("输入错误\n");
return 0;
}
printf("这个月的天数是: %d\n", days);
/************************************************/
int num;
printf("请输入一个整数:\n");
scanf_s("%d", &num);
if (num <= 1) {
printf("%d不是合数也不是质数\n", num);
}
else {
for (int i = 2; i*i <= num; i++) {
if (num % i == 0) {
printf("%d是合数\n", num);
return 0;
}
}
printf("%d是质数\n", num);
}
switch语句注意事项
switch语句注意事项
-
在每个case的末尾需加入break将此次的switch跳出,若利用continue跳出case,依然会执行下面的case。
-
switch后的变量类型为整形;枚举型;字符型,布尔型
-
case 后面必须是一个整数,或者是结果为整数的表达式,但不能包含任何变量。
+++
? : 运算符(三元运算符)
我们已经在前面的章节中讲解了 条件运算符 ? :,可以用来替代 if…else 语句。它的一般形式如下:
Exp1 ? Exp2 : Exp3;
其中,Exp1、Exp2 和 Exp3 是表达式。请注意,冒号的使用和位置。
? 表达式的值是由 Exp1 决定的。如果 Exp1 为真,则计算 Exp2 的值,结果即为整个表达式的值。如果 Exp1 为假,则计算 Exp3 的值,结果即为整个表达式的值。
//输入一个数字来判断它是否为奇数或偶数
#include<stdio.h>
int main()
{
int num;
printf("输入一个数字 : ");
scanf("%d",&num);
(num%2==0)?printf("偶数"):printf("奇数");
}
循环
循环类型
C 语言提供了以下几种循环类型。点击链接查看每个类型的细节。
循环类型 | 描述 |
---|---|
while 循环 | 当给定条件为真时,重复语句或语句组。它会在执行循环主体之前测试条件。 |
for 循环 | 多次执行一个语句序列,简化管理循环变量的代码。 |
do…while 循环 | 除了它是在循环主体结尾测试条件外,其他与 while 语句类似。 |
嵌套循环 | 可以在 while、for 或 do…while 循环内使用一个或多个循环。 |
循环控制语句
循环控制语句改变你代码的执行顺序。通过它你可以实现代码的跳转。
C 提供了下列的循环控制语句。点击链接查看每个语句的细节。
控制语句 | 描述 |
---|---|
break 语句 | 终止循环或 switch 语句,程序流将继续执行紧接着循环或 switch 的下一条语句。 |
continue 语句 | 告诉一个循环体立刻停止本次循环迭代,重新开始下次循环迭代。 |
goto 语句 | 将控制转移到被标记的语句。但是不建议在程序中使用 goto 语句。 |
//使用三种循环,输出1 - 100中可以被7整除的数
printf("while\n ");
int i = 1;
while (i != 101)
{
i++;
if (i % 7 == 0)
printf("%d ", i);
}
printf("\ndo while\n ");
i = 1;
do
{
i++;
if (i % 7 == 0)
printf("%d ", i);
} while (i != 101);
printf("\nfor\n ");
for (i = 1;i != 101;i++)
{
if (i % 7 == 0)
printf("%d ", i);
}
//输出100 - 999之间所有的水仙花数。水仙花数指的是:各位数字立方和等于该数本身例如:3、153 = 1 * 1 * 1 + 5 * 5 * 5 + 3 * 3 * 3.
int a = 0, b = 0, c = 0;
printf("\n");
for (i = 100; i <= 999; i++)
{
a = i/100;
b = i %100/ 10;
c = i %100%10;
if(a*a*a+b*b*b+c*c*c==i)
printf("%d ",i);
}
//输出Fibonacci(斐波那契)数列的前20个数。这个数列有如下特点。第1、2个数为1,1。从第3个数开始,该数是其前面两个数之和,。即该数列为1,1,2,3,5,8,13,21,34。
a = 1; b = 1;
printf("\n");
int d = 10;
for (i = 0; i <= d; i++)
{
printf("%d %d ", a, b);
a = a + b;
b = a + b;
}
// 猜数字游戏,给出一个数字确切的数字,并给出范围,让用户去猜,猜对了,输出成功,猜错了,继续猜,最多猜10次。
i = 0;
printf("\n猜数0-100\n");
while (i != 10)
{
scanf_s("%d", &a);
i++;
if (a == 50)
{
printf("猜对了\n");
break;
}
else
{
printf("继续猜次数为%d\n", 10 - i);
}
if (a < 50)
{
printf("小了\n");
}
else
printf("大了\n");
}
// 输出9*9乘法口诀
for (i = 1; i < 10; i++ )
{
for (a = 1; a <= i; a++ )
{
printf("%d*%d=%d ", a, i, i * a);
}
printf("\n");
}
D:
i++;
printf("%d ",i++);
goto D;
数组
数组类型
在C语言中,数组是一种用于存储相同类型元素的数据结构。元素的类型可以是基本数据类型(如整数、浮点数)或者用户自定义的数据类型。
数组定义
数组的定义格式为:
<数据类型> <数组名>[<数组大小>];
其中:
<数据类型>
表示数组中元素的数据类型。<数组名>
是数组的标识符。<数组大小>
表示数组能够容纳的元素个数。
示例:
int numbers[5]; // 定义一个包含5个整数的数组
float grades[10]; // 定义一个包含10个浮点数的数组
char characters[8]; // 定义一个包含8个字符的数组
数组初始化
数组的初始化可以在定义时进行,也可以在后续的代码中逐个元素进行初始化。
定义时初始化:
int numbers[5] = {1, 2, 3, 4, 5}; // 定义并初始化数组
后续初始化:
float grades[3];
grades[0] = 85.5;
grades[1] = 90.0;
grades[2] = 78.5;
数组遍历
使用循环结构可以方便地遍历数组中的元素,执行相同或类似的操作。
int numbers[5] = {1, 2, 3, 4, 5};
for (int i = 0; i < 5; i++) {
// 对数组中的元素执行操作
printf("%d ", numbers[i]);
}
数组赋值
数组的赋值可以通过逐个元素的方式进行,也可以使用循环结构进行批量赋值。
int sourceArray[3] = {10, 20, 30};
int targetArray[3];
// 逐个元素赋值
targetArray[0] = sourceArray[0];
targetArray[1] = sourceArray[1];
targetArray[2] = sourceArray[2];
或者使用循环:
for (int i = 0; i < 3; i++) {
targetArray[i] = sourceArray[i];
}
注意事项
- 数组下标从0开始,因此数组元素的访问和操作需要注意不越界。
- 数组的大小应该在定义时确定,并且不能在运行时改变。
- 初始化数组时,如果提供的初始值数量少于数组大小,剩余元素会被自动初始化为0。
- 数组在函数传递中通常以指针的形式传递,因此在函数内部对数组的修改会影响到原数组。
strcpy函数
strcpy
函数用于将一个字符串复制到另一个字符串数组中。它确实会更改目标数组的内容。请注意,strcpy
操作是按字节逐个复制的,直到遇到源字符串的空字符(\0
)为止。
以下是一个使用 strcpy
的简单示例:
#include <stdio.h>
#include <string.h>
int main() {
char source[] = "Hello, World!";
char destination[20];
strcpy(destination, source);
printf("Source: %s\n", source);
printf("Destination: %s\n", destination);
return 0;
}
在这个例子中,destination
数组通过 strcpy
函数被改变,它现在包含与 source
相同的字符串。
请注意以下几点:
- 确保目标数组足够大: 在使用
strcpy
时,确保目标数组有足够的空间来容纳源字符串以及源字符串的空字符。否则可能导致缓冲区溢出。 - 字符串结束标志:
strcpy
在复制字符串时会复制源字符串的空字符(\0
),因此目标字符串会以空字符结尾。 - 指针传递: 由于
strcpy
操作是按字节逐个进行的,如果你传递的是指向数组的指针而不是数组本身,同样也会更改原始数组。
#include <stdio.h>
#include <string.h>
void modifyString(char* str) {
strcpy(str, "Modified!");
}
int main() {
char myString[20] = "Original";
printf("Before: %s\n", myString);
modifyString(myString);
printf("After: %s\n", myString);
return 0;
}
上面的例子中,modifyString
函数通过 strcpy
改变了传递给它的字符串数组。
函数
1. 函数的定义
在C语言中,函数是一组执行特定任务的语句块,可以通过函数名调用。函数的定义通常包括返回类型、函数名、参数列表和函数体。
// 函数定义
int add(int a, int b) {
return a + b;
}
2. 函数的声明
在使用函数之前,通常需要在代码中声明函数的原型。函数原型包括返回类型、函数名和参数列表,但不包含函数体。
// 函数声明
int add(int a, int b);
3. 注意事项
3.1 返回类型
函数定义和声明中需要指定函数的返回类型。如果函数不返回任何值,返回类型应为void
。
// 有返回值的函数
int add(int a, int b) {
return a + b;
}
// 无返回值的函数
void greet() {
printf("Hello!\n");
}
3.2 参数传递
参数可以通过值传递或引用传递到函数中。在C语言中,参数默认是按值传递的,但可以通过指针实现引用传递。
// 值传递
void modifyValue(int x) {
x = x * 2;
}
// 引用传递
void modifyValueByReference(int *x) {
*x = *x * 2;
}
4. 函数的调用
函数的调用是通过函数名和传递给函数的参数列表来完成的。调用时,传递的参数数量和类型必须与函数定义或声明中的参数列表相匹配。
int result = add(3, 4);
5. 函数嵌套
在一个函数中调用另一个函数称为函数嵌套。这种方式可以提高代码的模块化和可读性。
int multiply(int a, int b) {
return a * b;
}
int calculate(int x, int y) {
int temp = add(x, y);
return multiply(temp, 2);
}
main()的隐藏参数
在C语言中,标准规定main
函数的原型为:
int main(int argc, char *argv[])
这表示main
函数接受两个参数:argc
和argv
。
argc
(argument count)是一个整数,表示命令行参数的数量,包括程序名本身。argv
(argument vector)是一个指向字符指针数组的指针,其中每个指针指向一个命令行参数字符串。
所以,正确的main
函数声明应该是:
int main(int argc, char *argv[]) {
// 函数体
return 0;
}
尽管在实际编写时,也可以使用void
来声明main
函数,但标准规定的形式包含这两个参数,它们对于处理命令行参数非常重要。通常情况下,我们会使用上述带参数的形式。
指针
1. 指针的定义
在C语言中,指针是一种特殊的变量类型,其存储的数值是内存地址。通过指针,我们可以直接访问和修改内存中的数据,为程序提供了更灵活的内存管理机制。
指针的声明方式如下:
int *ptr; // 声明一个整型指针
char *charPtr; // 声明一个字符型指针
2. 指针的初始化
指针变量在声明时可以进行初始化,也可以在后续的代码中赋值。初始化可以通过将变量的地址赋给指针来完成。
int num = 10;
int *ptr = # // 指向整型变量num的指针
3. 指针的类型
指针的类型必须与其所指向的变量类型相匹配。例如,如果指针指向整型变量,那么指针的类型应该是整型指针。
int num = 10;
int *ptr = # // 正确,整型指针指向整型变量
char *charPtr = # // 错误,类型不匹配
4. 注意事项
4.1 空指针
空指针是指未初始化的指针,其值为NULL。在使用指针之前,最好先将其初始化为NULL,以避免出现悬空指针的问题。
int *ptr = NULL; // 初始化为NULL
4.2 指针算术
指针可以进行加法和减法运算,但要谨慎确保不越界或访问无效内存。
int arr[5] = {1, 2, 3, 4, 5};
int *ptr = arr;
// 合法的指针算术
ptr++; // 指向arr[1]
ptr = ptr + 2; // 指向arr[3]
// 非法,可能越界
// ptr = ptr + 10;
4.3.野指针
野指针是指未经初始化的指针,或者指向已被释放的内存的指针。使用野指针可能导致程序崩溃或产生不可预测的结果。
int *wildPtr; // 野指针,未初始化
// 释放内存后继续使用指针
int *ptr = (int *)malloc(sizeof(int));
free(ptr);
*ptr = 10; // 野指针,指向已释放的内存
6. 函数指针
函数指针指向函数而不是变量,允许在运行时动态选择调用的函数。函数指针的声明和使用如下:
#include <stdio.h>
// 函数原型
int add(int a, int b) {
return a + b;
}
int subtract(int a, int b) {
return a - b;
}
int main() {
// 声明函数指针
int (*operation)(int, int);
// 初始化函数指针
operation = add;
printf("Addition: %d\n", operation(5, 3));
operation = subtract;
printf("Subtraction: %d\n", operation(5, 3));
return 0;
}
6. 示例代码
下面是一个简单的示例代码,演示了指针的基本用法:
#include <stdio.h>
int main() {
int num = 42;
int *ptr = #
printf("Value of num: %d\n", num);
printf("Address of num: %p\n", &num);
printf("Value of num using pointer: %d\n", *ptr);
printf("Address stored in pointer: %p\n", ptr);
return 0;
}
结构体
1. 结构体的定义与重命名
结构体是一种用户自定义的数据类型,允许将不同类型的数据组合在一起形成一个新的数据类型。结构体的定义使用struct
关键字。
// 结构体定义与重命名
typedef struct {
char name[50];
int age;
float height;
} Person;
通过typedef
关键字,可以为结构体定义起一个更简洁的别名,这里我们将struct Person
重命名为Person
。
2. 结构体的声明和初始化
结构体的声明与初始化与之前相似,但使用了上面定义的别名。
// 结构体声明与初始化
Person person1;
Person person2 = {"John", 25, 175.5};
3. 结构体的访问
结构体的成员可以通过成员运算符.
来访问,同样使用了重命名后的结构体名称。
printf("Name: %s\n", person2.name);
printf("Age: %d\n", person2.age);
printf("Height: %.2f\n", person2.height);
4. 结构体嵌套与重命名
结构体可以嵌套在其他结构体中,使用了typedef
后的别名可以使代码更加简洁。
// 结构体嵌套与重命名
typedef struct {
int day;
int month;
int year;
} Date;
typedef struct {
char name[50];
int age;
Date birthdate;
} Student;
5. 结构体与指针
使用指针来操作结构体时,同样可以使用重命名后的别名。
// 结构体与指针
Person *ptrPerson = &person1;
printf("Name: %s\n", ptrPerson->name);
6. 示例代码
下面是一个带有typedef
重命名的示例代码:
#include <stdio.h>
// 结构体定义与重命名
typedef struct {
char name[50];
int age;
float height;
} Person;
int main() {
// 结构体声明与初始化
Person person1;
Person person2 = {"John", 25, 175.5};
// 结构体访问
printf("Name: %s\n", person2.name);
printf("Age: %d\n", person2.age);
printf("Height: %.2f\n", person2.height);
// 结构体嵌套与重命名
typedef struct {
int day;
int month;
int year;
} Date;
typedef struct {
char name[50];
int age;
Date birthdate;
} Student;
// 结构体与指针
Person *ptrPerson = &person1;
printf("Name: %s\n", ptrPerson->name);
return 0;
}
枚举
1. 什么是枚举?
枚举是一种用户定义的数据类型,用于定义一组命名的整数常量。枚举可以提高代码的可读性和可维护性,使程序员能够使用有意义的标识符来表示不同的状态或值。
2. 枚举的语法
enum 枚举名 {
标识符1,
标识符2,
// 更多标识符
};
3. 枚举的使用示例
示例1:定义一个星期枚举
#include <stdio.h>
enum Weekdays {
Monday,
Tuesday,
Wednesday,
Thursday,
Friday,
Saturday,
Sunday
};
int main() {
enum Weekdays today = Wednesday;
switch (today) {
case Monday:
printf("今天是星期一。\n");
break;
case Tuesday:
printf("今天是星期二。\n");
break;
case Wednesday:
printf("今天是星期三。\n");
break;
// 更多的case语句
default:
printf("今天是休息日。\n");
}
return 0;
}
示例2:定义一个颜色枚举
#include <stdio.h>
enum Colors {
Red,
Green,
Blue,
Yellow,
Purple
};
int main() {
enum Colors myColor = Blue;
if (myColor == Red) {
printf("我的颜色是红色。\n");
} else if (myColor == Blue) {
printf("我的颜色是蓝色。\n");
} else {
printf("我的颜色是其他颜色。\n");
}
return 0;
}
示例3:枚举常量的赋值
#include <stdio.h>
enum Months {
January = 1,
February,
March,
April,
May,
June,
July,
August,
September,
October,
November,
December
};
int main() {
enum Months currentMonth = March;
printf("当前月份的值是:%d\n", currentMonth);
return 0;
}
宏定义
1. 什么是宏定义?
宏定义是C语言中一种预处理指令,用于创建代码片段的简单替代。通过宏定义,可以在程序中使用简短的标识符来表示一段代码,从而提高代码的可读性和可维护性。
2. 宏定义的语法
#define 标识符 替代内容
标识符
是宏定义的名称,通常用大写字母表示,以便和变量名区分。替代内容
是宏定义的实际代码片段,可以是表达式、语句、或任何有效的C语言代码。
3. 宏定义的使用
3.1 简单宏定义
#include <stdio.h>
#define PI 3.14159
int main() {
double radius = 5.0;
double area = PI * radius * radius;
printf("圆的面积:%f\n", area);
return 0;
}
3.2 带参数的宏定义
#include <stdio.h>
#define SQUARE(x) ((x) * (x))
int main() {
int num = 4;
int result = SQUARE(num);
printf("%d的平方:%d\n", num, result);
return 0;
}
3.3 字符串宏定义
#include <stdio.h>
#define GREETING "Hello, C Macros!"
int main() {
printf("%s\n", GREETING);
return 0;
}
4. 宏定义的注意事项
- 宏定义是简单的文本替代,没有类型检查,容易引发错误。
- 使用括号来确保宏定义在表达式中的正确展开,避免优先级问题。
- 避免在宏定义中使用多条语句,以免引发不可预料的错误。
- 尽量使用const常量或枚举替代宏定义,以提高代码的可维护性。
5. 预定义宏
C语言中有一些预定义的宏,如 __FILE__
、__LINE__
、__DATE__
等,它们提供有关代码信息的常量。
#include <stdio.h>
int main() {
printf("当前文件:%s\n", __FILE__);
printf("当前行号:%d\n", __LINE__);
printf("编译日期:%s\n", __DATE__);
return 0;
}
内存管理
c代码的编译过程
-
预处理
-
- 宏定义展开、头文件展开、条件编译,这里并不会检查语法
-
编译
-
- 检查语法,将预处理后文件编译生成汇编文件
-
汇编
-
- 将汇编文件生成目标文件(二进制文件)
-
链接
-
- 将目标文件链接为可执行程序
内存的分区
1、内存有两种:物理内存 虚拟内存
- 物理内存:实实在在存在的存储设备
- 虚拟内存:操作系统虚拟出来的内存
操作系统会在物理内存和虚拟内存之间做映射,在32位系统下 ,每个进程的寻址范围是4G,
0x00 00 00 00 ~0xff ff ff ff
当然写应用程序的时候,我们看到的是虚拟内存
2、在运行程序的时候,操作系统会将虚拟内存进行区分,划分为5个分区,分别是:
代码区(text)、数据区(data)、未初始化数据区(bss)、堆区(heap)、栈区(stack)
有人有时候直接把数据区data和未初始化数据区bss合起来叫做静态区或者全局区
-
代码区(text segment)
-
- 加载的是可执行文件代码段,所有的可执行代码都加载到代码区,这块内存是不可以在运行期间修改的。
-
未初始化数据区(BSS)
-
- 加载的是可执行文件BSS段,位置可以分开亦可以紧靠数据段,存储于数据段的数据(全局未初始化,静态未初始化数据)的生存周期为整个程序运行过程。
-
全局初始化数据区/静态数据区(data segment)
-
- 加载的是可执行文件数据段,存储于数据段(全局初始化,静态初始化数据,文字常量(只读))的数据的生存周期为整个程序运行过程。
-
栈区(stack)
-
- 栈是一种先进后出的内存结构,由编译器自动分配释放,存放函数的参数值、返回值、局部变量等。在程序运行过程中实时加载和释放,因此,局部变量的生存周期为申请到释放该段栈空间。
-
堆区(heap)
-
- 堆是一个大容器,它的容量要远远大于栈,但没有栈那样先进后出的顺序。用于动态内存分配。堆在内存中位于BSS区和栈区之间。一般由程序员分配和释放
在运行程序的时候,操作系统会将 虚拟内存进行分区。
1).堆(heap) 在动态申请内存的时候,在堆里开辟内存。
2).栈(stack) 主要存放局部变量。
3).静态全局区
1:未初始化的静态全局区 (bss)
静态变量(定义变量的时候,前面加 static 修饰),或全局变量 ,没有初始化的,存在此区
2:初始化的静态全局区 (data)
全局变量、静态变量,赋过初值的,存放在此区
4).代码区(text segment) 存放咱们的程序代码
5).文字常量区(data) 存放常量的
1. 内存管理概述
在C语言中,程序员负责手动管理内存,包括分配和释放内存。为了完成这些任务,C语言提供了一组标准的内存管理函数,它们包含在头文件 <stdlib.h>
中。
2. 内存分配函数
2.1 malloc函数
malloc
(memory allocation)函数用于动态分配指定字节数的内存空间,并返回指向该内存块的指针。
#include <stdlib.h>
int *arr = (int *)malloc(5 * sizeof(int));
if (arr == NULL) {
// 内存分配失败的处理
} else {
// 内存分配成功,arr指向一个包含5个整数的内存块
}
// 使用完内存后,记得释放
free(arr);
2.2 calloc 函数
calloc
(contiguous allocation)函数用于分配指定数量和大小的连续内存空间,并返回指向该内存块的指针。
#include <stdlib.h>
int *arr = (int *)calloc(5, sizeof(int));
if (arr == NULL) {
// 内存分配失败的处理
} else {
// 内存分配成功,arr指向一个包含5个整数的内存块,且已清零
}
// 使用完内存后,记得释放
free(arr);
2.3 realloc函数
realloc
(reallocation)函数用于重新分配已分配内存的大小。
#include <stdlib.h>
int *arr = (int *)malloc(5 * sizeof(int));
if (arr == NULL) {
// 内存分配失败的处理
} else {
// 内存分配成功,arr指向一个包含5个整数的内存块
// 重新分配内存大小为10个整数
arr = (int *)realloc(arr, 10 * sizeof(int));
}
// 使用完内存后,记得释放
free(arr);
3. 内存释放函数
3.1 free函数
free
函数用于释放之前通过 malloc
、calloc
或 realloc
分配的内存。
#include <stdlib.h>
int *arr = (int *)malloc(5 * sizeof(int));
if (arr != NULL) {
// 使用内存
// 释放内存
free(arr);
} else {
// 内存分配失败的处理
}
4. 注意事项
- 使用动态分配的内存后,务必使用相应的释放函数,以避免内存泄漏。
- 在释放内存后,及时将指针设为
NULL
,以防止野指针的问题。