那些看似不起眼的波澜的日复一日,会突然在某一天让人看到坚持的意义
初阶 | 目录: | — |
---|---|---|
分支语句 | 循环语句 | goto语句 |
函数 | 数组 | 操作符 |
指针 | 结构体 |
1.分支语句:
1.什么是语句
C语句可分为以下五类:1.表达式语句2. 函数调用语句 3.控制语句 4.复合语句 5.空语句
控制语句用于控制程序的执行流程,以实现程序的各种结构方式(C语言支持三种结构:顺序结构、选
择结构、循环结构),它们由特定的语句定义符组成,C语言有九种控制语句。
可分成以下三类:
- 条件判断语句也叫分支语句:if语句、switch语句;
- 循环执行语句:do while语句、while语句、for语句;
- 转向语句:break语句、goto语句、continue语句、return语句。
2.1分支语句(选择结构)
//if语法结构:
if(表达式)
语句;
if else结构:
if(表达式)
语句1;
else
语句2;
多分支结构:
if(表达式1)
语句1;
else if(表达式2)
语句2;
else
语句3;
在C语言中,“else”关键字用于条件语句中,与“if”关键字配合使用,以执行条件为真或假时的不同操作。 详细介绍:
基本用法。在基本的“if-else”结构中,如果“if”条件为真,则执行“if”语句块中的代码;如果条件为假,则执行“else”语句块中的代码。
嵌套用法。可以在“if-else”结构中嵌套另一个“if-else”结构,以便在某个条件下进行更深入的判断。这种嵌套可以有一个或多个“else”语句,具体取决于需要的逻辑复杂性。
固定搭配。在C语言中,“else”通常与“if”搭配使用,构成“if-else”结构。如果“if”条件为真,则执行“if”语句块中的代码;如果条件为假,则执行“else”语句块中的代码。这种结构是C语言中条件判断的基础。
此外,“else”还可以与“elseif”结合使用,以增加更多的条件判断。例如,可以判断一个学生的成绩等级,只有当所有条件都不满足时,才会执行“else”语句块中的代码。这种结构提高了代码的效率和可读性。
else的匹配:else是和它离的最近的if匹配的。
3.1 switch语句
switch语句也是一种分支语句。
常常用于多分支的情况。
//语法结构:
switch(整型表达式)
{
语句项;
}
case 整形常量表达式:
语句;
break语句的实际效果是把语句列表划分为不同的分支部分。
3.3 default语句
default:
写在任何一个 case 标签可以出现的位置。
当 switch 表达式的值并不匹配所有 case 标签的值时,这个 default 子句后面的语句就会执行。
所以,每个switch语句中只能出现一条default子句。
但是它可以出现在语句列表的任何位置,而且语句流会像执行一个case标签一样执行default子句
2.循环语句
章节目录: | 循环语句 | <返回 |
---|---|---|
1.while循环 | 2.for循环 | 3.do…while |
1.1break和continue | 2.1break和continue | 3.1break和continue |
1.1 while循环
//while 语法结构
while(表达式)
循环语句;
while语句执行流程:
while语句的执行流程首先评估while语句中的条件表达式,如果表达式的值为真(或非零),则执行紧跟其后的循环体语句。
循环体执行完毕后,控制流程回到while语句,再次评估条件表达式,如果条件依然为真,则重复执行循环体;如果条件为假(或零),则终止循环,继续执行while语句后面的程序代码。
在使用while循环时,确保循环条件最终会变为假,以避免无限循环或死循环,还要注意缩进和语法正确性,以确保循环的正常执行。
while语句流程图:
break在while循环中的作用:
其实在循环中只要遇到break,就停止后期的所有的循环,直接终止循环。
所以:while中的break是用于永久终止循环的。
continue在while循环中的作用就是:
continue是用于终止本次循环的,也就是本次循环中continue后边的代码不会再执行,
而是直接跳转到while语句的判断部分。进行下一次循环的入口判断。
3.1 for循环
//for循环语法结构
for(表达式1; 表达式2; 表达式3)
循环语句;
表达式1
表达式1为初始化部分,用于初始化循环变量的。
表达式2
表达式2为条件判断部分,用于判断循环时候终止。
表达式3
表达式3为调整部分,用于循环条件的调整。
for循环执行流程:
-
初始化。在循环开始前执行一次初始化语句,用于设置循环控制变量的初始值。
-
条件判断。评估循环条件,如果条件为真,则执行循环体;如果条件为假,则终止循环。
执行循环体。当条件判断为真时,执行此处的代码块。
-
末尾循环体。执行完循环体后,可能会执行一次末尾循环体,这取决于具体实现。
-
迭代更新。在每次循环迭代后执行,用于更新循环控制变量的值,然后回到条件判断步骤。
通过这个过程,for循环可以实现重复执行某段代码,直到满足某个条件后停止
for循环执行流程图:
在for循环中,break语句的作用是强制终止当前层的循环,并继续执行循环外的代码。
在for循环中,continue语句的作用是:跳过循环体中剩余的语句,结束此次循环,并强制进入下一次循环。
**3.3 do...while()循环**
do
循环语句;
while(表达式);
do…while执行流程:
在执行循环体之前会测试条件表达式的值,并在条件为真时执行循环体。循环体至少会执行一次,即使条件表达式的初始值就为假。
do…while执行流程图:
do语句的特点:循环至少执行一次
3.3.1 do while循环中的break和continue
在do…while语句中,break语句的作用是强制终止当前层的循环,并继续执行循环外的代码。
在do…while语句中,continue语句的作用是:跳过循环体中剩余的语句,结束此次循环,并强制进入下一次循环。
<返回
3.goto语句
1.1goto语句简介
是一种在编程中用于改变程序执行流程的控制语句。它的基本格式是goto
语句标号;,其中“语句标号”是一个按标识符规则书写的符号,放在某行代码前,后跟冒号“:”。goto语句的作用是根据这个标号无条件地跳转到相应的代码行,继续执行后续的代码。在c语言中,goto语句可以与if语句,for语句,while语句,等配合使用,实现条件转移、构成循环或跳出循环体等功能。然而,在结构化程序设计中,过度使用goto语句可能会导致程序流程变得混乱,增加理解和调试的难度,因此通常不建议使用。
4.函数
章节目录: | 函数 | <返回 |
---|---|---|
1.函数是什么 | 2.函数的分类 | 3.函数的定义 |
4.函数的参数 | 5.函数的调用 | 6.函数的嵌套调用和链式访问 |
7.函数递归 |
维基百科中对函数的定义:子程序
-
在计算机科学中,子程序(英语:Subroutine, procedure, function, routine, method,
subprogram, callable unit),是一个大型程序中的某部分代码, 由一个或多个语句块组
成。它负责完成某项特定任务,而且相较于其他代 码,具备相对的独立性。
-
一般会有输入参数并有返回值,提供对过程的封装和细节的隐藏。这些代码通常被集成为软
件库。
2.1库函数
库函数(Library function)是将函数封装入库,供用户使用的一种方式。**具体是指把一些常用到的函数编写完成后放到一个文件里,供不同的人进行调用。调用的时候把它所在的文件名用#include指令加到里面即可。库函数可以极大地提高程序员的开发效率和程序的质量,一般分为静态库和动态库两种类型。在C语言中,库函数可以是C语言标准规定的库函数,也可以是编译器特定的库函数。12
库函数可以屏蔽底层操作系统的差异,为程序员提供一个统一的接口,使得程序员可以更加专注于业务逻辑的实现,而不用关心底层的细节。例如,不同的操作系统可能提供了不同的API来访问文件系统,但是通过库函数,程序员可以使用统一的接口来访问文件系统,从而提高了程序的可移植性。
C语言常用的库函数都有:
- IO函数
- 字符串操作函数
- 字符操作函数
- 内存操作函数
- 时间/日期函数
- 数学函数
- 其他库函数
**注:**使用库函数,必须包含 #include 对应的头文件。
在C语言中,自定义函数的定义一般格式为:“类型 <函数名>(<形式参数表>){<语句序列>}”。例如,定义一个函数计算两个整数的和可以这样写:“int add(int a, int b){return a + b;}”这个函数名为add,接受两个整型参数a和b,返回它们的和。
函数的组成:
ret_type fun_name(para1, * )
{
statement;//语句项
}
ret_type 返回类型
fun_name 函数名
para1 函数参数
3.1 实际参数(实参):
真实传给函数的参数,叫实参。
实参可以是:常量、变量、表达式、函数等。
无论实参是何种类型的量,在进行函数调用时,它们都必须有确定的值,以便把这些值传送给形
参
3.2 形式参数(形参):
形式参数是指函数名后括号中的变量,因为形式参数只有在函数被调用的过程中才实例化(分配内
存单元),所以叫形式参数。形式参数当函数调用完成之后就自动销毁了。因此形式参数只在函数
中有效。
形参实例化之后其实相当于实参的一份临时拷贝。
4.1 传值调用
函数的形参和实参分别占有不同内存块,对形参的修改不会影响实参。
4.2 传址调用
- 传址调用是把函数外部创建变量的内存地址传递给函数参数的一种调用函数的方式。
- 这种传参方式可以让函数和函数外边的变量建立起真正的联系,也就是函数内部可以直接操
- 作函数外部的变量。
函数和函数之间可以根据实际的需求进行组合的,也就是互相调用的
5.1 嵌套调用
函数的嵌套调用是指在一个函数内部调用另一个函数,而被调用的函数又可以再次调用其他函数,以此类推,形成多个函数互相嵌套的调用关系。
例如,有三个函数funca,funca和funcc,在funca中调用了funcb,funcb中又调用了funcc,funcc负责执行具体的操作,如输出"Hello
World"。需要注意的是,虽然某些编程语言(如C语言)不允许在函数内部嵌套定义另一个函数,但仍然允许函数之间的嵌套调用。在函数嵌套调用的过程中,需要注意的是,当函数a调用函数b时,b中的局部变量和参数将被带到b的执行上下文中,当b执行完毕后,其局部变量和参数将被销毁,然后控制权返回给a,a中的执行继续进行
函数可以嵌套调用,但是不能嵌套定义。
5.2 链式访问
把一个函数的返回值作为另外一个函数的参数
6.1 函数声明:
- 告诉编译器有一个函数叫什么,参数是什么,返回类型是什么。但是具体是不是存在,函数
声明决定不了。
函数的声明一般出现在函数的使用之前。要满足先声明后使用。
函数的声明一般要放在头文件中的。
6.2 函数定义:
函数的定义是指函数的具体实现,交待函数的功能实现。
放置函数的声明
#ifndef __TEST_H__
#define __TEST_H__
//函数的声明
int Add(int x, int y);
#endif //__TEST_H__
放置函数的实现
#include "test.h"
//函数Add的实现
int Add(int x, int y)
{
return x+y;
}
7.1 什么是递归
程序调用自身的编程技巧称为递归( recursion)。
递归做为一种算法在程序设计语言中广泛应用。 一个过程或函数在其定义或说明中有直接或间接
调用自身的一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问
题来求解,递归策略只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程
序的代码量。
递归的主要思考方式在于:把大事化小
7.2 递归的两个必要条件
- 存在限制条件,当满足这个限制条件的时候,递归便不再继续。
- 每次递归调用之后越来越接近这个限制条件
7.3 递归与迭代
递归与迭代都是基于控制结构:迭代用重复结构,而递归用选择结构。递归与迭代都涉及重复:迭代显式使用重复结构,而递归通过重复函数调用实现重复。递归与迭代都涉及终止测试:迭代在循环条件失败时终止,递归在遇到基本情况时终止。使用计数器控制重复的迭代和递归都逐渐到达终止点:迭代一直修改计数器,直到计数器值使循环条件失败;递归不断产生最初问题的简化副本,直到达到基本情况。迭代和递归过程都可以无限进行:如果循环条件测试永远不变成false,则迭代发生无限循环;如果递归永远无法回推到基本情况,则发生无穷递归。递归函数是通过调用函数自身来完成任务,而且在每次调用自身时减少任务量。而迭代是循环的一种形式,这种循环不是由用户输入而控制,每次迭代步骤都必须将剩余的任务减少;也就是说,循环的每一步都必须执行一个有限的过程,并留下较少的步骤。
关于递归的一些题目:
1.求n的阶乘。
2.求第n个斐波那契数。
3.求字符串的长度。
4.接受一个整型值(无符号),按照顺序打印它的每一位。
题解:
<求字符串长度>
<n的阶乘>
<斐波那契数>
<接受一个整型值,按照顺序打印它的每一位>
函数递归的经典题目
汉诺塔问题用
题解:
<汉诺塔>
5.数组
章节目录: | <返回 | |
---|---|---|
1.一维数组的创建 | 2.数组的初始化 | 3.数组在内存中的存储 |
4.二维数组的创建 | 5.数组的初始化 | 6.数组在内存中的存储 |
7.数组越界 | 8.数组作为函数参数 | 9.数组名是什么 |
1. 一维数组的创建和初始化。
数组是一组相同类型元素的集合。
数组的创建方式:
type_t arr_name [const_n];
//type_t 是指数组的元素类型
//const_n 是一个常量表达式,用来指定数组的大小
注:数组创建,在C99标准之前, [] 中要给一个常量才可以,不能使用变量。在C99标准支持了变长数
组的概念,数组的大小可以使用变量指定,但是数组不能初始化。
数组的初始化是指,在创建数组的同时给数组的内容一些合理初始值(初始化)。
例如:
int arr1[10] = {1,2,3};
int arr2[] = {1,2,3,4};
int arr3[5] = {1,2,3,4,5};
char arr4[3] = {'a',98, 'c'};
char arr5[] = {'a','b','c'};
char arr6[] = "abcdef";
数组在创建的时候如果想不指定数组的确定的大小就得初始化。数组的元素个数根据初始化的内容来确
定。
**一维数组在内存中的存储方式是按照数组的下标有序存放。**例如,对于声明为int a;
的数组,系统会为该数组分配10个连续的整数型内存单元。数组的第一个元素位于内存中的a
位置,第二个元素位于a
位置,依此类推,直到最后一个元素a
。每个数组元素都相当于一个同类型的简单变量,它们都存储在一块连续的内存空间中。
图示:
2. 二维数组的创建和初始化
//数组创建
int arr[3][4];
char arr[3][5];
double arr[2][4];
//数组初始化
int arr[3][4] = {1,2,3,4};
int arr[3][4] = {{1,2},{4,5}};
int arr[][4] = {{2,3},{4,5}};//二维数组如果有初始化,行可以省略,列不能省略
二维数组在概念上是二维的,而存储器单元是按一维线性排列的。 如何在一维存储器中存放二维数组,可有两种方式:一种是按行排列, 即放完一行之后顺次放入第二行。另一种是按列排列, 即放完一列之后再顺次放入第二列。
数组的下标是有范围限制的。
数组的下规定是从0开始的,如果数组有n个元素,最后一个元素的下标就是n-1。
所以数组的下标如果小于0,或者大于n-1,就是数组越界访问了,超出了数组合法空间的访问。
C语言本身是不做数组下标的越界检查,编译器也不一定报错,但是编译器不报错,并不意味着程序就
是正确的,
注:二维数组的行和列也可能存在越界。
数组元素的作用与变量相当,一般来说,凡是变量可以出现的地方,都可以用数组元素代替。
因此,数组元素也可以用作函数实参,其用法与变量相同,向形参传递数组元素的值。
此外,数组名也可以用作形参和实参,传递的是数组第一个元素的地址。
数组名是数组首元素的地址。(有两个例外)
- sizeof(数组名),计算整个数组的大小,sizeof内部单独放一个数组名,数组名表示整个数
组。
- &数组名,取出的是数组的地址。&数组名,数组名表示整个数组。
除此1,2两种情况之外,所有的数组名都表示数组首元素的地址
—练习:
<冒泡排序>
6.操作符
章节目录: | 操作符 | <返回 |
---|---|---|
1.操作符分类 | 2算术操作符 | 3. 移位操作符 |
4.位操作符 | 5.赋值操作符 | 6.单目操作符 |
7.关系操作符 | 8. 逻辑操作符 | 9. 条件操作符 |
10.逗号表达式 | 11. 下标引用,函数,结构 | 12. 表达式求值 |
算术操作符
移位操作符
位操作符
赋值操作符
单目操作符
关系操作符
逻辑操作符
条件操作符
逗号表达式
下标引用、函数调用和结构成员
+ - * / %
-
除了 % 操作符之外,其他的几个操作符可以作用于整数和浮点数。
-
对于 / 操作符如果两个操作数都为整数,执行整数除法。而只要有浮点数执行的就是浮点数除法。
-
% 操作符的两个操作数必须为整数。返回的是整除之后的余数。
<< 左移操作符
>> 右移操作符
注:移位操作符的操作数只能是整数。
3.1 左移操作符
移位规则:
左边抛弃、右边补0
3.2 右移操作符
移位规则:
首先右移运算分两种:
- 逻辑移位
左边用0填充,右边丢弃
- 算术移位
左边用原该值的符号位填充,右边丢弃
警告⚠ :
对于移位运算符,不要移动负数位,这个是标准未定义的。
例如:
int num = 10;
num>>-1;//error
位操作符有:
& //按位与
| //按位或
^ //按位异或
注:他们的操作数必须是整数。
位与运算符(&)可以对两个操作数的每个对应位执行逻辑与操作,只有当两个位都为1时,结果位才为1,否则为0。
位或运算符(|)则对两个操作数的每个对应位执行逻辑或操作,只要两个位中至少有一个为1,结果位就为1。
位或运算符(^ )是一种逻辑运算符,用于比较两个值。如果两个值不相同,则异或结果为1;如果两个值相同,则异或结果为0。
赋值操作符是一个很棒的操作符,他可以让你得到一个你之前不满意的值。也就是你可以给自己重新赋
值。
复合赋值符
+=
-=
*=
/=
%=
>>=
<<=
&=
|=
^=
这些运算符都可以写成复合的效果
例如:
int x = 10;
x = x + 10;
x += 10;//复合赋值
//效果一样
! 逻辑反操作
- 负值
+ 正值
& 取地址
sizeof 操作数的类型长度(以字节为单位)
~ 对一个数的二进制按位取反
-- 前置、后置--
++ 前置、后置++
* 间接访问操作符(解引用操作符)
(类型) 强制类型转换
sizeof:求变量(类型)所占空间的大小
++和–运算符
前置++和–
++:先自加后使用
–:先自减后使用
后置++和–
++:先使用后自加
–:先使用后自减
>
>=
<
<=
!= 用于测试“不相等”
== 用于测试“相等”
逻辑操作符有哪些:
&& 逻辑与
|| 逻辑或
区分逻辑与和按位与
区分逻辑或和按位或
逻辑与和或的特点:
&&判断操作数如果都为真,才为真
||判断操作数有一个为真,才为真
三目操作符:exp1 ? exp2 : exp3
如果exp1为真就将exp2赋给exp1
否则就将exp3赋给exp2
exp1, exp2, exp3, …expN
逗号表达式,就是用逗号隔开的多个表达式。
逗号表达式,从左向右依次执行。整个表达式的结果是最后一个表达式的结果。
- 下标引用操作符
操作数:一个数组名 + 一个索引值
示例:
int arr[10];//创建数组
arr[9] = 10;//实用下标引用操作符。
[ ]的两个操作数是arr和9。
- ( ) 函数调用操作符
接受一个或者多个操作数:第一个操作数是函数名,剩余的操作数就是传递给函数的参数
- 访问一个结构的成员
. 结构体.成员名
-> 结构体指针->成员名
表达式求值的顺序一部分是由操作符的优先级和结合性决定。
同样,有些表达式的操作数在求值的过程中可能需要转换为其他类型
12.1 隐式类型转换
C的整型算术运算总是至少以缺省整型类型的精度来进行的。
为了获得这个精度,表达式中的字符和短整型操作数在使用之前被转换为普通整型,这种转换称为整型
提升。
整型提升的意义:
表达式的整型运算要在CPU的相应运算器件内执行,CPU内整型运算器(ALU)的操作数的字节长度
一般就是int的字节长度,同时也是CPU的通用寄存器的长度。
因此,即使两个char类型的相加,在CPU执行时实际上也要先转换为CPU内整型操作数的标准长
度。
通用CPU(general-purpose CPU)是难以直接实现两个8比特字节直接相加运算(虽然机器指令
中可能有这种字节相加指令)。所以,表达式中各种长度可能小于int长度的整型值,都必须先转
换为int或unsigned int,然后才能送入CPU去执行运算。
//实例
char a,b,c;
...
a=b+c;
b和c的值被提升为普通整型,然后再执行加法运算。
加法运算完成之后,结果将被截断,然后再存储于a中
如何进行整体提升呢?
整形提升是按照变量的数据类型的符号位来提升的
//负数的整形提升
char c1 = -1;
变量c1的二进制位(补码)中只有8个比特位:
1111111
因为 char 为有符号的 char
所以整形提升的时候,高位补充符号位,即为1
提升之后的结果是:
11111111111111111111111111111111
//正数的整形提升
char c2 = 1;
变量c2的二进制位(补码)中只有8个比特位:
00000001
因为 char 为有符号的 char
所以整形提升的时候,高位补充符号位,即为0
提升之后的结果是:
00000000000000000000000000000001
//无符号整形提升,高位补0
12.2 算术转换
如果某个操作符的各个操作数属于不同的类型,那么除非其中一个操作数的转换为另一个操作数的类
型,否则操作就无法进行。下面的层次体系称为寻常算术转换。
long double
double
float
unsigned long int
long int
unsigned int
int
如果某个操作数的类型在上面这个列表中排名较低,那么首先要转换为另外一个操作数的类型后执行运
算。
警告:
但是算术转换要合理,要不然会有一些潜在的问题。
float f = 3.14;
int num = f;//隐式转换,会有精度丢失
12.3 操作符的属性
复杂表达式的求值有三个影响的因素。
-
操作符的优先级
-
操作符的结合性
-
是否控制求值顺序。
两个相邻的操作符先执行哪个?取决于他们的优先级。如果两者的优先级相同,取决于他们的结合性。
操作符优先级
指针
章节目录: | 指针 | <返回 |
---|---|---|
1.指针的概念 | 2.指针类型 | 3.野指针 |
4.指针运算 | 5.指针和数组 | 6.二级指针 |
7.指针数组 |
指针是什么?
指针理解的2个要点:
指针是内存中一个最小单元的编号,也就是地址
平时口语中说的指针,通常指的是指针变量,是用来存放内存地址的变量
总结:指针就是地址,口语中说的指针通常指的是指针变量
指针变量
我们可以通过&(取地址操作符)取出变量的内存起始地址,把地址可以存放到一个变量中,这个
变量就是指针变量
int main()
{
int a = 10;//在内存中开辟一块空间
int *p = &a;//这里我们对变量a,取出它的地址,可以使用&操作符。
//a变量占用4个字节的空间,这里是将a的4个字节的第一个字节的地址存放在p变量
中,p就是一个之指针变量。
return 0;
}
总结:
指针变量,用来存放地址的变量。(存放在指针中的值都被当成地址处理)。
指针变量是用来存放地址的,地址是唯一标示一个内存单元的。
指针的大小在32位平台是4个字节,在64位平台是8个字节
指针类型是根据它所指向的数据类型来定义的。例如,char*
是一个指向 char
类型数据的指针,int*
是一个指向 int
类型数据的指针,float*
是一个指向 float
类型数据的指针,等等。每种类型的指针都有固定的大小,通常是基于它指向的数据类型的大小。例如,在大多数现代系统上,int*
和 float*
类型的指针都是4字节,而 char*
类型的指针是1字节。
2.1 指针±整数
//演示实例
int main()
{
int n = 10;
char *pc = (char*)&n;
int *pi = &n;
printf("%p\n", &n);
printf("%p\n", pc);
printf("%p\n", pc+1);
printf("%p\n", pi);
printf("%p\n", pi+1);
return 0;
}
请添加图片描述
总结:指针的类型决定了指针向前或者向后走一步有多大(距离)
2.2 指针的解引用
总结:
指针的类型决定了,对指针解引用的时候有多大的权限(能操作几个字节)。
比如: char* 的指针解引用就只能访问一个字节,而 int* 的指针的解引用就能访问四个字节。
概念: 野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
3.1 野指针成因
- 指针未初始化
例如:
int main()
{
int *p;//局部变量指针未初始化,默认为随机值
*p = 20;
return 0;
}
- 指针越界访问
#include <stdio.h>
int main()
{
int arr[10] = {0};
int *p = arr;
int i = 0;
for(i=0; i<=11; i++)
{
//当指针指向的范围超出数组arr的范围时,p就是野指针
*(p++) = i;
}
return 0;
}
- 指针指向的空间释放
这里放在动态内存开辟的时候讲解,这里可以简单提示一下
3.2 如何规避野指针
-
指针初始化
-
小心指针越界
-
指针指向空间释放,及时置NULL
-
避免返回局部变量的地址
-
指针使用之前检查有效性
- 指针± 整数
- 指针-指针
- 指针的关系运算
4.1 指针±整数
在C语言中,指针和整数可以进行加法和减法运算。指针的加减运算实际上是改变指针所指向的内存地址。
指针加整数:
当你给一个指针加上一个整数,它的值(即它所指向的内存地址)会增加。例如,如果你有一个指向整型数组的指针,并且你给这个指针加上1,它将指向数组中的下一个元素。这是因为每个整型元素通常占用4个字节(这取决于编译器和平台),所以指针的地址会增加4个字节。
int arr[5] = {1, 2, 3, 4, 5};
int *p = arr; // p指向arr[0]
p = p + 1; // p现在指向arr[1]
指针减整数:
类似地,当你从一个指针中减去一个整数,它的值(即它所指向的内存地址)会减少。例如,如果你有一个指向整型数组的指针,并且你给这个指针减去1,它将指向数组中的前一个元素。
int arr[5] = {1, 2, 3, 4, 5};
int *p = arr + 4; // p指向arr[4]
p = p - 1; // p现在指向arr[3]
注意:
- 对指针进行加减运算时,要确保结果指针仍在有效的内存范围内,否则可能会导致未定义的行为。
- 指针的加减运算与数组索引是等价的。例如,
arr[i]
等价于*(arr + i)
。 - 不同类型的指针进行加减运算时,要注意每个类型的大小可能不同,因此增加的字节数也不同。例如,
double
类型指针加1时,地址会增加8个字节(假设double
是8字节的)。
4.2 指针-指针
在C语言中,当你从一个指针中减去另一个指针时,实际上得到的是两个指针之间的差值,这个差值是以指针所指向的元素类型为单位的。换句话说,你得到的是两个指针之间相隔多少个这种类型的元素。
这里有一些要点需要注意:
- 两个指针必须指向同一类型的元素,或者至少是可以安全地进行大小比较的类型。
- 两个指针必须指向同一块连续的内存区域,或者其中一个指针是另一个指针的偏移。
- 结果是一个整数值,表示两个指针之间相差的元素个数。
下面是一个简单的例子,展示了如何在C语言中计算两个指针之间的差值:
#include <stdio.h>
int main() {
int arr[5] = {1, 2, 3, 4, 5};
int *p1 = arr; // p1指向arr[0]
int *p2 = arr + 3; // p2指向arr[3]
// 计算p1和p2之间的差值
int diff = p2 - p1;
printf("The difference between p2 and p1 is: %d\n", diff);
return 0;
}
在这个例子中,p1
指向arr
数组的第一个元素,而p2
指向arr
数组的第四个元素。因此,p2 - p1
的结果是3
,表示p2
指针相对于p1
指针向前移动了3个整数元素。
请注意,如果你试图对不相干或未初始化的指针进行减法操作,结果将是未定义的,可能会导致程序崩溃或产生不可预测的结果。因此,始终确保你在做指针减法操作时,两个指针是相关的(即它们指向同一块内存或其中一个是另一个的偏移)。
4.3 指针的关系运算
在C语言中,指针是一种非常重要的数据类型,它存储了某个变量在内存中的地址。由于指针实质上存储的是地址,所以你可以对指针进行某些特定的关系运算,如比较运算。但需要注意的是,这些关系运算通常只适用于指向同一类型或兼容类型的指针。
以下是几种常见的指针关系运算:
- 指针相等比较 (
==
和!=
):
你可以使用相等比较运算符==
来判断两个指针是否指向同一个内存地址。同样,你可以使用不等比较运算符!=
来判断两个指针是否不指向同一个内存地址。
int a = 10;
int b = 20;
int *ptr1 = &a;
int *ptr2 = &b;
if (ptr1 == ptr2) {
// 这段代码不会执行,因为ptr1和ptr2指向不同的内存地址
}
if (ptr1 != ptr2) {
// 这段代码会执行,因为ptr1和ptr2指向不同的内存地址
}
- 指针大小比较 (
<
,>
,<=
,>=
):
这些比较运算符在指针上的使用取决于指针的类型。对于指向同一类型或兼容类型的指针,这些运算符可以用于比较它们指向的内存地址的大小。例如,一个指向数组元素的指针可以与其他指向同一数组元素的指针进行比较。
int arr[] = {1, 2, 3, 4, 5};
int *ptr1 = &arr[0];
int *ptr2 = &arr[3];
if (ptr1 < ptr2) {
// 这段代码会执行,因为ptr1指向的内存地址小于ptr2指向的内存地址
}
然而,对于指向不同类型的指针,使用这些比较运算符可能会导致编译错误或未定义的行为。因此,在实际编程中,你应该避免对指向不同类型或不兼容类型的指针进行关系运算。
另外,要注意的是,即使两个指针指向的内存地址相等(即 ptr1 == ptr2
返回 true
),也不能保证它们指向的数据具有相同的值。因此,在对指针进行比较时,你需要明确你正在比较的是指针本身(即内存地址)还是指针指向的数据。
指针和数组在C和C++等语言中有着非常密切的关系,因为数组的名字在大多数上下文中会被解释为指向数组第一个元素的指针。下面我将详细解释这种关系以及如何在编程中使用它们。
数组和指针的基本关系
-
数组名作为指针:
当数组名作为表达式的一部分使用时,它通常会被解释为指向数组第一个元素的指针。例如:int arr[5] = {1, 2, 3, 4, 5}; int *p = arr; // p指向arr的第一个元素,即arr[0]
-
通过指针访问数组元素:
可以使用指针算术来访问数组中的不同元素。指针加1会使其指向下一个元素:int *ptr = arr; int first_element = *ptr; // first_element = arr[0] int second_element = *(ptr + 1); // second_element = arr[1]
-
数组和指针的长度:
需要注意的是,指针本身并不知道它指向的数组的长度。因此,如果你有一个指向数组的指针,并且想要遍历整个数组,你需要知道数组的长度或者使用某种方式来检测数组的结束。
二级指针,也称为指向指针的指针,是一个指针变量,它存储的是另一个指针变量的地址。二级指针在C和C++中主要用于处理指针数组、动态分配的内存块、字符串数组和函数指针数组等情况。
二级指针的定义
int **ptr;
在这个例子中,ptr
是一个二级指针,它指向一个整型指针。换句话说,ptr
存储的是一个整型指针的地址。
二级指针的使用场景
-
指针数组:
当你有一个指针数组,并且你想要操作这个数组本身(例如,你想改变数组中某个指针的地址),你需要使用二级指针。int *arr[3]; int **pptr = arr; // pptr指向arr的第一个元素(即一个指针) // 修改arr数组中的第一个指针所指向的地址 *pptr = malloc(10 * sizeof(int));
-
动态内存分配:
当你动态分配一个指针数组时,你通常会使用二级指针来接收malloc
或calloc
返回的地址。int **ptrArray = malloc(10 * sizeof(int *)); for (int i = 0; i < 10; i++) { ptrArray[i] = malloc(10 * sizeof(int)); }
-
字符串数组:
处理字符串数组时,每个字符串可能由字符指针表示,而整个字符串数组可以由二级指针处理。char **strArray = malloc(5 * sizeof(char *)); for (int i = 0; i < 5; i++) { strArray[i] = malloc(100 * sizeof(char)); strcpy(strArray[i], "Hello, World!"); }
-
函数指针数组:
如果你有一个函数指针数组,并且想要改变其中的函数指针,你会使用二级指针。void (*funcArray[2])() = {func1, func2}; void (**pFuncArray)() = funcArray; pFuncArray[0] = func3; // 改变funcArray数组中的第一个函数指针
二级指针的解引用
要访问二级指针所指向的指针所指向的值,你需要进行两次解引用。
int a = 10;
int *ptr1 = &a;
int **ptr2 = &ptr1;
// 通过二级指针访问a的值
int value = **ptr2; // value 现在是 10
在这个例子中,**ptr2
首先解引用 ptr2
得到 ptr1
,然后再解引用 ptr1
得到 a
的值。
总的来说,二级指针是C和C++中处理指针的高级工具,主要用于处理指针数组和动态分配的内存。正确使用二级指针可以简化代码,提高内存管理的灵活性。但也要注意,由于它们涉及多层的间接引用,编程时需要特别小心以避免出现错误或内存泄漏。
int* arr3[5];
指针数组是在C语言中,数组元素全为指针变量的数组。这些指针变量可以指向任何数据类型,包括基本数据类型、结构体、类等。指针数组中的每个元素都是指针,指向相应的数据。
一维指针数组的定义形式为:“类型名 *数组标识符[数组长度]”。例如,int* ptr_array[10]
定义了一个包含10个整型指针的一维指针数组。每个元素都是一个指针,可以通过 *ptr_array[i]
来访问第i个指针所指向的值。
指针数组非常适合用来指向若干个字符串,这样可以使字符串处理更加方便、灵活。例如,可以用指针数组来存储一组字符串的地址,然后通过指针数组来访问和修改这些字符串。
指针数组也可以作为函数的参量使用,使用方式与普通数组类似。在函数中,可以通过指针数组来传递一组数据的地址,从而实现在函数内部对这些数据的修改。
需要注意的是,指针数组与数组指针是两种不同的概念。数组指针是指向数组首元素地址的指针,其本质为指针;而指针数组是数组元素为指针的数组,其本质为数组。在使用时,需要根据具体需求选择合适的类型。
结构体
章节目录 | 结构体 | <返回 |
---|---|---|
1.定义结构体类型 | 2.声明结构体变量 | 3.初始化结构体变量 |
4.指定成员初始值 | 5.逐个成员赋值 | 6.嵌套结构体初始化 |
7.结构体数组初始化 | 8.结构体成员的访问 | 9.结构体指针使用 |
10.结构体传参 |
结构的基础知识
结构是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量
在C语言中,结构体是一种复合数据类型,它允许您将不同类型的数据组合成一个单一的类型。结构体常用于创建记录(如员工记录、学生记录等),其中每个记录包含多个相关的数据字段。
首先,您需要定义结构体类型。这可以通过使用struct
关键字来完成。
struct Student {
char name[50];
int age;
float gpa;
};
上述代码定义了一个名为Student
的结构体类型,它有三个成员:name
(字符数组),age
(整数),和gpa
(浮点数)。
一旦您定义了结构体类型,就可以声明该类型的变量。
struct Student stu1;
这行代码声明了一个名为stu1
的Student
类型的变量。
在声明结构体变量时,可以对其进行初始化。
struct Student stu2 = {"Tom", 20, 3.5};
您也可以在初始化时只指定某些成员的值,其余成员将被自动初始化为它们的默认值(对于数值类型,默认值为0;对于字符数组,默认值为空字符)。
struct Student stu3 = {"Jerry", 0, 0.0}; // age和gpa将被初始化为0
如果您在声明结构体变量时没有进行初始化,或者您想在后续的代码中更改其值,可以逐个成员地为其赋值。
stu3.age = 21;
stu3.gpa = 3.8;
结构体可以包含其他结构体作为成员。当进行初始化时,需要遵循相应的嵌套结构。
struct Address {
char street[50];
char city[50];
};
struct StudentWithAddress {
struct Student student;
struct Address address;
};
struct StudentWithAddress stu4 = {{"Tom", 20, 3.5}, {"123 Main St", "Anytown"}};
您可以创建结构体数组,并为每个元素提供初始化值。
struct Student students[] = {
{"Tom", 20, 3.5},
{"Jerry", 21, 3.8}
};
结构体变量访问成员
结构变量的成员是通过点操作符(.)访问的。点操作符接受两个操作数。
例如:
我们可以看到 s 有成员 name 和 age
那我们如何访问s的成员?
struct S s;
strcpy(s.name, "zhangsan");//使用.访问name成员
s.age = 20;//使用.访问age成员
您可以使用指针来操作结构体变量。首先,需要创建一个指向结构体类型的指针,然后将其指向一个结构体变量。
struct Student *pStu = &stu1;
pStu->age = 22; // 等同于 stu1.age = 22;
上述代码首先创建了一个指向Student
类型的指针pStu
,然后将其指向stu1
。接着,通过指针pStu
修改了stu1
的age
成员的值。
总结:结构体是C语言中非常有用的工具,允许您将不同类型的数据组合在一起。通过掌握结构体的定义、声明、初始化和使用指针,您可以更加灵活地处理复杂的数据结构。
在C语言中,结构体变量在函数调用时可以作为参数传递。传递结构体变量给函数主要有三种方式:
- 传值方式(Pass by Value)
当结构体变量以传值方式传递给函数时,会创建该结构体变量的一个副本,并将副本传递给函数。这意味着函数内对结构体的修改不会影响原始的结构体变量。
void printStudent(struct Student s) {
printf("Name: %s, Age: %d, GPA: %.2f\n", s.name, s.age, s.gpa);
}
int main() {
struct Student stu = {"Tom", 20, 3.5};
printStudent(stu); // 传递结构体变量的副本
return 0;
}
由于传值方式会创建结构体的副本,因此对于大型结构体来说,这种传递方式可能效率低下,因为需要复制整个结构体的内容。
- 传指针方式(Pass by Pointer)
传指针方式是指将指向结构体变量的指针传递给函数。这样,函数内部可以直接操作原始的结构体变量。
void printStudent(struct Student *s) {
printf("Name: %s, Age: %d, GPA: %.2f\n", s->name, s->age, s->gpa);
}
int main() {
struct Student stu = {"Tom", 20, 3.5};
printStudent(&stu); // 传递结构体变量的地址
return 0;
}
传指针方式更加高效,因为它避免了复制整个结构体。同时,它允许函数修改原始的结构体变量。
- 传结构体数组
当需要传递结构体数组时,可以传递数组的首个元素的指针。函数内部可以通过指针访问整个数组。
void printStudents(struct Student *students, int count) {
for (int i = 0; i < count; i++) {
printf("Name: %s, Age: %d, GPA: %.2f\n", students[i].name, students[i].age, students[i].gpa);
}
}
int main() {
struct Student students[] = {
{"Tom", 20, 3.5},
{"Jerry", 21, 3.8}
};
printStudents(students, 2); // 传递数组首元素的地址和数组长度
return 0;
}
在传递结构体数组时,通常还需要传递数组的长度,以便函数知道需要处理多少个元素。
在选择传递方式时,需要根据具体需求来权衡。传值方式在函数调用时会复制结构体,而传指针方式则更高效,可以直接操作原始数据。然而,传指针方式也要求程序员更加小心地处理指针,以避免潜在的问题,如野指针和内存泄漏等。
函数传参的时候,参数是需要压栈的。
如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的
下降。
结构体传参的时候,要传结构体的地址。
<返回