做调查挣钱的网站,附近做广告的电话,网站开发流程主要分成什么,珠海网站友情链接一、C语言基础知识入门
c语言基础知识入门一经出现就以其功能丰富、表达能力强、灵活方便、应用面广等特点迅速在全世界普及和推广。C语言不但执行效率高而且可移植性好#xff0c;可以用来开发应用软件、驱动、操作系统等#xff0c;2024年C语言基础知识入门大全。C语言基础… 一、C语言基础知识入门
c语言基础知识入门一经出现就以其功能丰富、表达能力强、灵活方便、应用面广等特点迅速在全世界普及和推广。C语言不但执行效率高而且可移植性好可以用来开发应用软件、驱动、操作系统等2024年C语言基础知识入门大全。C语言基础知识入门也是其它众多高级语言的鼻祖语言所以说学习C语言基础知识入门是进入编程世界的必修课
需 要 C语言基础知识入门大全 PDF版 和 详 细 教 程 的可以看一下文章结尾 二、C语言基础知识入门大全的具体结构
简单来说一个C程序就是由若干头文件和函数组成。 #include stdio.h就是一条预处理命令, 它的作用是通知C语言编译系统在对C程序进行正式编译之前需做一些预处理工作。 函数就是实现代码逻辑的一个小的单元。
三、C语言基础知识入门大全主函数
一个C程序有且只有一个主函数即main函数。 C程序就是执行主函数里的代码也可以说这个主函数就是C语言中的唯一入口。而main前面的int就是主函数的类型.printf()是格式输出函数这里就记住它的功能就是在屏幕上输出指定的信息return是函数的返回值根据函数类型的不同返回的值也是不同的。\n是转义字符中的换行符。(注意C程序一定是从主函数开始执行的)
四、C语言基础知识入门大全规范
一个说明或一个语句占一行例如包含头文件、一个可执行语句结束都需要换行。函数体内的语句要有明显缩进通常以按一下Tab键为一个缩进。括号要成对写如果需要删除的话也要成对删除。当一句可执行语句结束的时候末尾需要有分号。代码中所有符号均为英文半角符号。 五、C语言基础知识入门程序解释——注释
注释是写给程序员看的不是写给电脑看的。
C语言注释方法有两种 多行注释 /* 注释内容 */ 单行注释 //注释一行 六、C语言基础知识入门的标识符
C语言规定标识符可以是字母(AZaz)、数字(09)、下划线_组成的字符串并且第一个字符必须是字母或下划线。在使用标识符时还有注意以下几点
标识符的长度最好不要超过8位因为在某些版本的C中规定标识符前8位有效当两个标识符前8位相同时则被认为是同一个标识符。标识符是严格区分大小写的。例如Imooc和imooc 是两个不同的标识符。标识符最好选择有意义的英文单词组成做到见名知意不要使用中文。标识符不能是C语言的关键字。想了解更多C语言关键字的知识。
七、变量及赋值
变量就是可以变化的量而每个变量都会有一个名字标识符。变量占据内存中一定的存储单元。使用变量之前必须先定义变量要区分变量名和变量值是两个不同的概念。 变量定义的一般形式为数据类型 变量名;
多个类型相同的变量数据类型 变量名, 变量名, 变量名…; 注意:在定义中不允许连续赋值如int abc5;是不合法的。
变量的赋值分为两种方式:
先声明再赋值声明的同时赋值
八、基本数据类型
C语言中数据类型可分为
基本数据类型构造数据类型指针类型空类型四大类 最常用的整型, 实型与字符型(char,int,float,double): 整型数据是指不带小数的数字(int,short int,long int, unsigned int, unsigned short int,unsigned long int): 注
int short int long int是根据编译环境的不同所取范围不同。而其中short int和long int至少是表中所写范围, 但是int在表中是以16位编译环境写的取值范围。另外 c语言int的取值范围在于他占用的字节数 不同的编译器规定是不一样。ANSI标准定义int是占2个字节TC是按ANSI标准的它的int是占2个字节的。但是在VC里一个int是占4个字节的。
浮点数据是指带小数的数字。 生活中有很多信息适合使用浮点型数据来表示比如人的体重(单位公斤)、商品价格、圆周率等等。 因为精度的不同又分为3种(float,double,long double) 九、格式化输出语句
格式化输出语句也可以说是占位输出是将各种类型的数据按照格式化后的类型及指定的位置从计算机上显示。
其格式为printf(输出格式符输出项); 当输出语句中包含普通字符时可以采用以下格式
printf(普通字符输出格式符, 输出项);注意格式符的个数要与变量、常量或者表达式的个数一一对应
十、常量
在程序执行过程中值不发生改变的量称为常量。
mtianyan: C语言的常量可以分为直接常量和符号常量。
直接常量也称为字面量是可以直接拿来使用无需说明的量比如
整型常量13、0、-13实型常量13.33、-24.4字符常量‘a’、‘M’字符串常量”I love imooc!” 在C语言中可以用一个标识符来表示一个常量称之为符号常量。符号常量在使用之前必须先定义其一般形式为
#define 标识符 常量值 #include stdio.h
#define POCKETMONEY 10 //定义常量及常量值
int main()
{// POCKETMONEY 12; //小明私自增加零花钱对吗printf(小明今天又得到%d元零花钱\n, POCKETMONEY);return 0;
}符号常量不可以被改变。
十一、自动类型转换
数据类型存在自动转换的情况. 自动转换发生在不同数据类型运算时在编译的时候自动完成。 char类型数据转换为int类型数据遵循ASCII码中的对应值.
注: 字节小的可以向字节大的自动转换但字节大的不能向字节小的自动转换 char可以转换为intint可以转换为doublechar可以转换为double。但是不可以反向。 十二、强制类型转换
强制类型转换是通过定义类型转换运算来实现的。其一般形式为
(数据类型) (表达式)其作用是把表达式的运算结果强制转换成类型说明符所表示的类型
在使用强制转换时应注意以下问题
数据类型和表达式都必须加括号, 如把(int)(x/2y)写成(int)x/2y则成了把x转换成int型之后再除2再与y相加了。转换后不会改变原数据的类型及变量值只在本次运算中临时性转换。强制转换后的运算结果不遵循四舍五入原则。
十三、运算符号
C语言中运算符:
1.算术运算符
c语言基本运算符: 2.自增与自减运算符
自增运算符为其功能是使变量的值自增1自减运算符为--其功能是使变量值自减1。
它们经常使用在循环中。自增自减运算符有以下几种形式 3.赋值运算符
C语言中赋值运算符分为简单赋值运算符和复合赋值运算符
简单赋值运算符号了下面讲一下复合赋值运算符
复合赋值运算符就是在简单赋值符之前加上其它运算符构成.
注意复合运算符中运算符和等号之间是不存在空格的。
4.关系运算符
C语言中的关系运算符: 关系表达式的值是真和假在C程序用整数1和0表示。
注意, , , !这种符号之间不能存在空格。
5.逻辑运算符
C语言中的逻辑运算符: 6.三目运算符
C语言中的三目运算符?:其格式为
表达式1 ? 表达式2 : 表达式3; 执行过程是
先判断表达式1的值是否为真如果是真的话执行表达式2如果是假的话执行表达式3。
7.运算符大比拼之优先级比较
各种运算符号的顺序: 优先级别为1的优先级最高优先级别为10的优先级别最低。 十四、分支结构
1.简单if语句
C语言中的分支结构语句中的if条件语句。
简单if语句的基本结构如下
if(表达式)
{
执行代码块;
}其语义是如果表达式的值为真则执行其后的语句否则不执行该语句。
注意if()后面没有分号直接写{}
2.if-else语句
简单的if-else语句的基本结构: 语义是: 如果表达式的值为真则执行代码块1否则执行代码块2。
3.多重if-else语句
C语言中多重if-else语句其结构如下 语义是依次判断表达式的值当出现某个值为真时则执行对应代码块否则执行代码块n。
注意当某一条件为真的时候则不会向下执行该分支结构的其他语句。
4.嵌套if-else语句
C语言中嵌套if-else语句。嵌套if-else语句的意思就是在if-else语句中再写if-else语句。其一般形式为 十五、循环结构
1.while循环
反复不停的执行某个动作就是江湖人称的循环 。
C语言中有三种循环结构,先看一下C语言while循环的结构 其中表达式表示循环条件执行代码块为循环体。
while语句的语义是计算表达式的值当值为真(非0)时 执行循环体代码块。
while语句中的表达式一般是关系表达或逻辑表达式当表达式的值为假时不执行循环体反之则循环体一直执行。一定要记着在循环体中改变循环变量的值否则会出现死循环无休止的执行。循环体如果包括有一个以上的语句则必须用{}括起来组成复合语句。
2.do-while循环
C语言中的do-while循环一般形式如下 do-while循环语句的语义是:
它先执行循环中的执行代码块然后再判断while中表达式是否为真如果为真则继续循环如果为假则终止循环。因此do-while循环至少要执行一次循环语句。
注意mtianyan: 使用do-while结构语句时while括号后必须有分号。
3.for循环
c语言中for循环一般形式 它的执行过程如下
执行表达式1对循环变量做初始化判断表达式2若其值为真(非0)则执行for循环体中执行代码块然后向下执行若其值为假(0)则结束循环;执行表达式3(i)等对于循环变量进行操作的语句;执行for循环中执行代码块后执行第二步;第一步初始化只会执行一次。循环结束程序继续向下执行。
注意for循环中的两个分号一定要写
在for循环中:
表达式1是一个或多个赋值语句它用来控制变量的初始值表达式2是一个关系表达式它决定什么时候退出循环表达式3是循环变量的步进值定义控制循环变量每循环一次后按什么方式变化。这三部分之间用分号 ; 分开。
使用for语句应该注意
for循环中的“表达式1、2、3”均可不写为空但两个分号(;;)不能缺省。省略“表达式1循环变量赋初值”表示不对循环变量赋初始值。省略“表达式2(循环条件)”不做其它处理循环一直执行死循环。省略“表达式3(循环变量增减量)”不做其他处理循环一直执行死循环。表达式1可以是设置循环变量的初值的赋值表达式也可以是其他表达式。表达式1和表达式3可以是一个简单表达式也可以是多个表达式以逗号分割。表达式2一般是关系表达式或逻辑表达式但也可是数值表达式或字符表达式只要其值非零就执行循环体。各表达式中的变量一定要在for循环之前定义。
3.三种循环比较
while, do-while和for三种循环在具体的使用场合上是有区别的如下
在知道循环次数的情况下更适合使用for循环;在不知道循环次数的情况下适合使用while或者do-while循环:如果有可能一次都不循环应考虑使用while循环如果至少循环一次应考虑使用do-while循环。但是从本质上讲while,do-while和for循环之间是可以相互转换的。
4.多重循环
多重循环就是在循环结构的循环体中又出现循环结构。
在实际开发中一般最多用到三层重循环。
因为循环层数越多运行时间越长程序越复杂所以一般用2-3层多重循环就可以了。另外不同循环之间也是可以嵌套的。
多重循环在执行的过程中外层循环为父循环内层循环为子循环
**父循环一次子循环需要全部执行完直到跳出循环。**父循环再进入下一次子循环继续执行…
十六、结束语句
1.break语句
那么循环5次的时候需要中断不继续训练。在C语言中可以使用break语句进行该操作.
使用break语句时注意以下几点
在没有循环结构的情况下break不能用在单独的if-else语句中。在多层循环中一个break语句只跳出当前循环。
2.continue语句
那么循环5次的时候需要中断后继续训练。在C语言中可以使用continue语句进行该操作
continue语句的作用是结束本次循环开始执行下一次循环。
break语句与continue语句的区别是:
break是跳出当前整个循环continue是结束本次循环开始下一次循环。 十七、局部与全局
C语言中的变量按作用域范围可分为两种即局部变量和全局变量。局部变量也称为内部变量。局部变量是在函数内作定义说明的。其作用域仅限于函数内 离开该函数后再使用这种变量是非法的。在复合语句中也可定义变量其作用域只在复合语句范围内。 全局变量也称为外部变量它是在函数外部定义的变量。它不属于哪一个函数它属于一个源程序文件。其作用域是整个源程序。
十八、变量存储类别
mtianyan: C语言根据变量的生存周期来划分可以分为静态存储方式和动态存储方式。
静态存储方式是指在程序运行期间分配固定的存储空间的方式。静态存储区中存放了在整个程序执行过程中都存在的变量如全局变量。 动态存储方式是指在程序运行期间根据需要进行动态的分配存储空间的方式。动态存储区中存放的变量是根据程序运行的需要而建立和释放的通常包括函数形式参数自动变量函数调用时的现场保护和返回地址等。 C语言中存储类别又分为四类
自动auto、静态static、寄存器的register外部的extern。
十九、内部函数与外部函数
在C语言中不能被其他源文件调用的函数称谓内部函数 内部函数由static关键字来定义因此又被称谓静态函数形式为 static [数据类型] 函数名[参数] 这里的static是对函数的作用范围的一个限定限定该函数只能在其所处的源文件中使用因此在不同文件中出现相同的函数名称的内部函数是没有问题的。 在C语言中能被其他源文件调用的函数称谓外部函数 外部函数由extern关键字来定义形式为 extern [数据类型] 函数名([参数]) C语言规定在没有指定函数的作用范围时系统会默认认为是外部函数因此当需要定义外部函数时extern也可以省略。 静态变量只赋值一次
二十、数组初体验
程序中也需要容器只不过该容器有点特殊它在程序中是一块连续的大小固定并且里面的数据类型一致的内存空间它还有个好听的名字叫数组。可以将数组理解为大小固定所放物品为同类的一个购物袋在该购 物袋中的物品是按一定顺序放置的。
1.我们来看一下如何声明一个数组
数据类型 数组名称[长度];
数组只声明也不行啊看一下数组是如何初始化的。说到初始化C语言中的数组初始化是有三种形式的分别是
数据类型 数组名称[长度n] {元素1,元素2…元素n};数据类型 数组名称[] {元素1,元素2…元素n};数据类型 数组名称[长度n]; 数组名称[0] 元素1; 数组名称[1] 元素2; 数组名称[n-1] 元素n;
我们将数据放到数组中之后又如何获取数组中的元素呢
获取数组元素时 数组名称[元素所对应下标];
如初始化一个数组 int arr[3] {1,2,3}; 那么arr[0]就是元素1。
注意
数组的下标均以0开始 数组在初始化的时候数组内元素的个数不能大于声明的数组长度 mtianyan: 如果采用第一种初始化方式元素个数小于数组的长度时多余的数组元素初始化为0 在声明数组后没有进行初始化的时候静态static和外部extern类型的数组元素初始化元素为0自动auto类型的数组的元素初始化值不确定。
2.数组的遍历
数组就可以采用循环的方式将每个元素遍历出来而不用人为的每次获取指定某个位置上的元素例如我们用for循环遍历一个数组 注意以下几点
最好避免出现数组越界访问循环变量最好不要超出数组的长度.C语言的数组长度一经声明长度就是固定无法改变并且C语言并不提供计算数组长度的方法。
由于C语言是没有检查数组长度改变或者数组越界的这个机制可能会在编辑器中编译并通过但是结果就不能肯定了因此还是不要越界或者改变数组的长度
3.数组作为函数参数
数组可以由整个数组当作函数的参数也可以由数组中的某个元素当作函数的参数
整个数组当作函数参数即把数组名称传入函数中例如 数组中的元素当作函数参数即把数组中的参数传入函数中例如 数组作为函数参数时注意以下事项
数组名作为函数实参传递时函数定义处作为接收参数的数组类型形参既可以指定长度也可以不指定长度。数组元素作为函数实参传递时数组元素类型必须与形参数据类型一致。
4.字符串与数组
C语言中是没有办法直接定义字符串数据类型的但是我们可以使用数组来定义我们所要的字符串。一般有以下两种格式
char 字符串名称[长度] “字符串值”;char 字符串名称[长度] {‘字符1’,‘字符2’,…,‘字符n’,’\0’};
注意
[]中的长度是可以省略不写的采用第2种方式的时候最后一个元素必须是’\0’’\0’表示字符串的结束标志采用第2种方式的时候在数组中不能写中文。在输出字符串的时候要使用printf(“%s”,字符数组名字);或者puts(字符数组名字);。
5.mtianyan:字符串函数
常用的字符串函数如下(strlen,strcmp,strcpy,strcat,atoi): 使用字符串函数注意以下事项
strlen()获取字符串的长度在字符串长度中是不包括‘\0’而且汉字和字母的长度是不一样的strcmp()在比较的时候会把字符串先转换成ASCII码再进行比较,返回的结果为0表示s1和s2的ASCII码相等,返回结果为1表示s1比s2的ASCII码大,返回结果为-1表示s1比s2的ASCII码小strcpy()拷贝之后会覆盖原来字符串且不能对字符串常量进行拷贝strcat在使用时s1与s2指的内存空间不能重叠且s1要有足够的空间来容纳要复制的字符串
6.多维数组
多维数组的定义格式是 数据类型 数组名称[常量表达式1][常量表达式2]…[常量表达式n]; 定义了一个名称为num数据类型为int的二维数组。其中第一个[3]表示第一维下标的长度就像购物时分类存放的购物第二个[3]表示第二维下标的长度就像每个购物袋中的元素。 多维数组的初始化与一维数组的初始化类似也是分两种
数据类型 数组名称[常量表达式1][常量表达式2]…[常量表达式n] {{值1,…,值n},{值1,…,值n},…,{值1,…,值n}};数据类型 数组名称[常量表达式1][常量表达式2]…[常量表达式n]; 数组名称[下标1][下标2]…[下标n] 值;
多维数组初始化要注意以下事项
采用第一种始化时数组声明必须指定列的维数。mtianyan: 因为系统会根据数组中元素的总个数来分配空间当知道元素总个数以及列的维数后会直接计算出行的维数采用第二种初始化时数组声明必须同时指定行和列的维数。
二维数组定义的时候可以不指定行的数量但是必须指定列的数量
二十一、C语言最核心的指针
说到指针就不可能脱离开内存学会指针的人分为两种一种是不了解内存模型另外一种则是了解。
不了解的对指针的理解就停留在“指针就是变量的地址”这句话会比较害怕使用指针特别是各种高级操作。
而了解内存模型的则可以把指针用得炉火纯青
想学好C语言很关键就是搞懂内存、指针、还有各种编译链接
1、内存本质
编程的本质其实就是操控数据数据存放在内存中。
因此如果能更好地理解内存的模型以及 C 如何管理内存就能对程序的工作原理洞若观火从而使编程能力更上一层楼。
大家真的别认为这是空话我大一整年都不敢用 C 写上千行的程序也很抗拒写 C。
因为一旦上千行经常出现各种莫名其妙的内存错误一不小心就发生了 coredump...... 而且还无从排查分析不出原因。
相比之下那时候最喜欢 Java在 Java 里随便怎么写都不会发生类似的异常顶多偶尔来个 NullPointerException也是比较好排查的。
直到后来对内存和指针有了更加深刻的认识才慢慢会用 C 写上千行的项目也很少会再有内存问题了。过于自信
「指针存储的是变量的内存地址」这句话应该任何讲 C 语言的书都会提到吧。
所以要想彻底理解指针首先要理解 C 语言中变量的存储本质也就是内存。
1内存编址
计算机的内存是一块用于存储数据的空间由一系列连续的存储单元组成就像下面这样 每一个单元格都表示 1 个 Bit一个 bit 在 EE 专业的同学看来就是高低电位而在 CS 同学看来就是 0、1 两种状态。
由于 1 个 bit 只能表示两个状态所以大佬们规定 8个 bit 为一组命名为 byte。
并且将 byte 作为内存寻址的最小单元也就是给每个 byte 一个编号这个编号就叫内存的地址。 这就相当于我们给小区里的每个单元、每个住户都分配一个门牌号在生活中我们需要保证门牌号唯一这样就能通过门牌号很精准的定位到一家人。
同样在计算机中我们也要保证给每一个 byte 的编号都是唯一的这样才能够保证每个编号都能访问到唯一确定的 byte。
2内存地址空间
上面我们说给内存中每个 byte 唯一的编号那么这个编号的范围就决定了计算机可寻址内存的范围。
所有编号连起来就叫做内存的地址空间这和大家平时常说的电脑是 32 位还是 64 位有关。
早期 Intel 8086、8088 的 CPU 就是只支持 16 位地址空间寄存器和地址总线都是 16 位这意味着最多对 2^16 64 Kb 的内存编号寻址。
这点内存空间显然不够用后来80286 在 8086 的基础上将地址总线和地址寄存器扩展到了20 位也被叫做 A20 地址总线。
当时在写 mini os 的时候还需要通过 BIOS 中断去启动 A20 地址总线的开关。
但是现在的计算机一般都是 32 位起步了32 位意味着可寻址的内存范围是 2^32 byte 4GB。
所以如果你的电脑是 32 位的那么你装超过 4G 的内存条也是无法充分利用起来的。
好了这就是内存和内存编址。
3变量的本质
有了内存接下来我们需要考虑int、double 这些变量是如何存储在 0、1 单元格的。
在 C 语言中我们会这样定义变量
int a 999;
char c c;
当你写下一个变量定义的时候实际上是向内存申请了一块空间来存放你的变量。
我们都知道 int 类型占 4 个字节并且在计算机中数字都是用补码不了解补码的记得去百度表示的。
999 换算成补码就是0000 0011 1110 0111
这里有 4 个byte所以需要四个单元格来存储 有没有注意到我们把高位的字节放在了低地址的地方那能不能反过来呢
当然这就引出了大端和小端。
像上面这种将高位字节放在内存低地址的方式叫做大端反之将低位字节放在内存低地址的方式就叫做小端。 上面只说明了 int 型的变量如何存储在内存而 float、char 等类型实际上也是一样的都需要先转换为补码。
对于多字节的变量类型还需要按照大端或者小端的格式依次将字节写入到内存单元。
记住上面这两张图这就是编程语言中所有变量的在内存中的样子不管是 int、char、指针、数组、结构体、对象... 都是这样放在内存的。
2、指针是什么啥 变量放在哪上面我说定义一个变量实际就是向计算机申请了一块内存来存放。
那如果我们要想知道变量到底放在哪了呢可以通过运算符来取得变量实际的地址这个值就是变量所占内存块的起始地址。
PS: 实际上这个地址是虚拟地址并不是真正物理内存上的地址
我们可以把这个地址打印出来
printf(%x, a);
大概会是像这样的一串数字:0x7ffcad3b8f3c
上面说我们可以通过符号获取变量的内存地址那获取之后如何来表示这是一个地址而不是一个普通的值呢
也就是在 C 语言中如何表示地址这个概念呢
对就是指针你可以这样
int *pa a;
pa 中存储的就是变量 a 的地址也叫做指向 a 的指针。
在这里我想谈几个看起来有点无聊的话题
为什么我们需要指针直接用变量名不行吗 当然可以但是变量名是有局限的。
变量名的本质是什么 是变量地址的符号化变量是为了让我们编程时更加方便对人友好可计算机可不认识什么变量 a它只知道地址和指令。
所以当你去查看 C 语言编译后的汇编代码就会发现变量名消失了取而代之的是一串串抽象的地址。
你可以认为编译器会自动维护一个映射将我们程序中的变量名转换为变量所对应的地址然后再对这个地址去进行读写。
也就是有这样一个映射表存在将变量名自动转化为地址 a | 0x7ffcad3b8f3c
c | 0x7ffcad3b8f2c
h | 0x7ffcad3b8f4c
....
说的好
可是我还是不知道指针存在的必要性那么问题来了看下面代码:
int func(...) {...
};int main() {int a;func(...);
};
假设我有一个需求 要求在func 函数里要能够修改 main 函数里的变量 a这下咋整在 main 函数里可以直接通过变量名去读写 a 所在内存。 但是在 func 函数里是看不见a 的呀。 你说可以通过取地址符号将 a 的地址传递进去
int func(int address) {....
};int main() {int a;func(a);
};
这样在func 里就能获取到 a 的地址进行读写了。
理论上这是完全没有问题的但是问题在于:
编译器该如何区分一个 int 里你存的到底是 int 类型的值还是另外一个变量的地址即指针。
这如果完全靠我们编程人员去人脑记忆了会引入复杂性并且无法通过编译器检测一些语法错误。
而通过int * 去定义一个指针变量会非常明确这就是另外一个 int 型变量的地址。
编译器也可以通过类型检查来排除一些编译错误。
这就是指针存在的必要性。
实际上任何语言都有这个需求只不过很多语言为了安全性给指针戴上了一层枷锁将指针包装成了引用。
可能大家学习的时候都是自然而然的接受指针这个东西但是还是希望这段啰嗦的解释对你有一定启发。
同时在这里提点小问题
既然指针的本质都是变量的内存首地址即一个 int 类型的整数。 那为什么还要有各种类型呢 比如 int 指针float 指针这个类型影响了指针本身存储的信息吗 这个类型会在什么时候发挥作用 解引用
上面的问题就是为了引出指针解引用的。
pa中存储的是a变量的内存地址那如何通过地址去获取a的值呢
这个操作就叫做解引用在 C 语言中通过运算符 *就可以拿到一个指针所指地址的内容了。
比如*pa就能获得a的值。
我们说指针存储的是变量内存的首地址那编译器怎么知道该从首地址开始取多少个字节呢
这就是指针类型发挥作用的时候编译器会根据指针的所指元素的类型去判断应该取多少个字节。
如果是 int 型的指针那么编译器就会产生提取四个字节的指令char 则只提取一个字节以此类推。
下面是指针内存示意图: pa 指针首先是一个变量它本身也占据一块内存这块内存里存放的就是 a 变量的首地址。
当解引用的时候就会从这个首地址连续划出 4 个 byte然后按照 int 类型的编码方式解释。
别看这个地方很简单但却是深刻理解指针的关键。
举两个例子来详细说明
比如
float f 1.0;
short c *(short*)f;
你能解释清楚上面过程对于 f 变量在内存层面发生了什么变化吗或者 c 的值是多少1
实际上从内存层面来说f 什么都没变。
如图: 假设这是f 在内存中的位模式这个过程实际上就是把 f 的前两个 byte 取出来然后按照 short 的方式解释然后赋值给 c。
详细过程如下
f取得f 的首地址
(short*)f
上面第二步什么都没做这个表达式只是说
“噢我认为f这个地址放的是一个 short 类型的变量”
最后当去解引用的时候*(short*)f时编译器会取出前面两个字节并且按照 short 的编码方式去解释并将解释出的值赋给 c 变量。
这个过程 f的位模式没有发生任何改变变的只是解释这些位的方式。
当然这里最后的值肯定不是 1至于是什么大家可以去真正算一下。
那反过来这样呢
short c 1;
float f *(float*)c;
如图 具体过程和上述一样但上面肯定不会报错这里却不一定。
为什么
(float*)c会让我们从c 的首地址开始取四个字节然后按照 float 的编码方式去解释。
但是c是 short 类型只占两个字节那肯定会访问到相邻后面两个字节这时候就发生了内存访问越界。
当然如果只是读大概率是没问题的。
但是有时候需要向这个区域写入新的值比如
*(float*)c 1.0;
那么就可能发生 coredump也就是访存失败。
另外就算是不会 coredump这种也会破坏这块内存原有的值因为很可能这是是其它变量的内存空间而我们去覆盖了人家的内容肯定会导致隐藏的 bug。
如果你理解了上面这些内容那么使用指针一定会更加的自如。
3、结构体和指针
结构体内包含多个成员这些成员之间在内存中是如何存放的呢
比如
struct fraction {int num; // 整数部分int denom; // 小数部分
};struct fraction fp;
fp.num 10;
fp.denom 2;
这是一个定点小数结构体它在内存占 8 个字节这里不考虑内存对齐两个成员域是这样存储的 我们把 10 放在了结构体中基地址偏移为 0 的域2 放在了偏移为 4 的域。
接下来我们做一个这样的操作
((fraction*)(fp.denom))-num 5;
((fraction*)(fp.denom))-denom 12;
printf(%d\n, fp.denom); // 输出多少
上面这个究竟会输出多少呢自己先思考下噢~
接下来我分析下这个过程发生了什么 首先fp.denom表示取结构体 fp 中 denom 域的首地址然后以这个地址为起始地址取 8 个字节并且将它们看做一个 fraction 结构体。
在这个新结构体中最上面四个字节变成了 denom 域而 fp 的 denom 域相当于新结构体的 num 域。
因此
((fraction*)(fp.denom))-num 5
实际上改变的是 fp.denom而
((fraction*)(fp.denom))-denom 12
则是将最上面四个字节赋值为 12。
当然往那四字节内存写入值结果是无法预测的可能会造成程序崩溃因为也许那里恰好存储着函数调用栈帧的关键信息也可能那里没有写入权限。
大家初学 C 语言的很多 coredump 错误都是类似原因造成的。
所以最后输出的是 5。
为什么要讲这种看起来莫名其妙的代码
就是为了说明结构体的本质其实就是一堆的变量打包放在一起而访问结构体中的域就是通过结构体的起始地址也叫基地址然后加上域的偏移。
其实C、Java 中的对象也是这样存储的无非是他们为了实现某些面向对象的特性会在数据成员以外添加一些 Head 信息比如C 的虚函数表。
实际上我们是完全可以用 C 语言去模仿的。
这就是为什么一直说 C 语言是基础你真正懂了 C 指针和内存对于其它语言你也会很快的理解其对象模型以及内存布局。 4、多级指针
说起多级指针这个东西我以前上学的时候最多理解到 2 级再多真的会把我绕晕经常也会写错代码。
你要是给我写个这个int ******p 能把我搞崩溃我估计很多同学现在就是这种情况
其实多级指针也没那么复杂就是指针的指针的指针的指针......非常简单。
今天就带大家认识一下多级指针的本质。
首先我要说一句话没有多级指针这种东西指针就是指针多级指针只是为了我们方便表达而取的逻辑概念。
首先看下生活中的快递柜 这种大家都用过吧每个格子都有一个编号我们只需要拿到编号然后就能找到对应的格子取出里面的东西。
这里的格子就是内存单元编号就是地址格子里放的东西就对应存储在内存中的内容。
假设我把一本书放在了 03 号格子然后把 03 这个编号告诉你你就可以根据 03 去取到里面的书。
那如果我把书放在 05 号格子然后在 03 号格子只放一个小纸条上面写着「书放在 05 号」。
你会怎么做
当然是打开 03 号格子然后取出了纸条根据上面内容去打开 05 号格子得到书。
这里的 03 号格子就叫指针因为它里面放的是指向其它格子的小纸条地址而不是具体的书。
明白了吗
那我如果把书放在 07 号格子然后在 05 号格子 放一个纸条「书放在 07号」同时在03号格子放一个纸条「书放在 05号」 这里的 03 号格子就叫二级指针05 号格子就叫指针而 07 号就是我们平常用的变量。
依次可类推出 N 级指针。
所以你明白了吗同样的一块内存如果存放的是别的变量的地址那么就叫指针存放的是实际内容就叫变量。
int a;
int *pa a;
int **ppa pa;
int ***pppa ppa;
上面这段代码pa就叫一级指针也就是平时常说的指针ppa 就是二级指针。
内存示意图如下: 不管几级指针有两个最核心的东西 指针本身也是一个变量需要内存去存储指针也有自己的地址 指针内存存储的是它所指向变量的地址
这就是我为什么多级指针是逻辑上的概念实际上一块内存要么放实际内容要么放其它变量地址就这么简单。
怎么去解读int **a这种表达呢
int ** a 可以把它分为两部分看即int* 和 *a后面 *a 中的*表示 a 是一个指针变量前面的 int* 表示指针变量a
只能存放 int* 型变量的地址。
对于二级指针甚至多级指针我们都可以把它拆成两部分。
首先不管是多少级的指针变量它首先是一个指针变量指针变量就是一个*其余的*表示的是这个指针变量只能存放什么类型变量的地址。
比如int****a表示指针变量 a 只能存放int*** 型变量的地址。
5、指针与数组
1一维数组
数组是 C 自带的基本数据结构彻底理解数组及其用法是开发高效应用程序的基础。
数组和指针表示法紧密关联在合适的上下文中可以互换。
如下
int array[10] {10, 9, 8, 7};
printf(%d\n, *array); // 输出 10
printf(%d\n, array[0]); // 输出 10printf(%d\n, array[1]); // 输出 9
printf(%d\n, *(array1)); // 输出 9int *pa array;
printf(%d\n, *pa); // 输出 10
printf(%d\n, pa[0]); // 输出 10printf(%d\n, pa[1]); // 输出 9
printf(%d\n, *(pa1)); // 输出 9
在内存中数组是一块连续的内存空间 第 0 个元素的地址称为数组的首地址数组名实际就是指向数组首地址当我们通过array[1]或者*(array 1) 去访问数组元素的时候。
实际上可以看做 address[offset]address 为起始地址offset 为偏移量但是注意这里的偏移量offset 不是直接和 address相加而是要乘以数组类型所占字节数也就是 address sizeof(int) * offset。
学过汇编的同学一定对这种方式不陌生这是汇编中寻址方式的一种基址变址寻址。
看完上面的代码很多同学可能会认为指针和数组完全一致可以互换这是完全错误的。
尽管数组名字有时候可以当做指针来用但数组的名字不是指针。
最典型的地方就是在 sizeof:
printf(%u, sizeof(array));
printf(%u, sizeof(pa));
第一个将会输出 40因为 array包含有 10 个int类型的元素而第二个在 32 位机器上将会输出 4也就是指针的长度。
为什么会这样呢
站在编译器的角度讲变量名、数组名都是一种符号它们都是有类型的它们最终都要和数据绑定起来。
变量名用来指代一份数据数组名用来指代一组数据数据集合它们都是有类型的以便推断出所指代的数据的长度。
对数组也有类型我们可以将 int、float、char 等理解为基本类型将数组理解为由基本类型派生得到的稍微复杂一些的类型
数组的类型由元素的类型和数组的长度共同构成。而 sizeof 就是根据变量的类型来计算长度的并且计算的过程是在编译期而不会在程序运行时。
编译器在编译过程中会创建一张专门的表格用来保存变量名及其对应的数据类型、地址、作用域等信息。
sizeof 是一个操作符不是函数使用 sizeof 时可以从这张表格中查询到符号的长度。
所以这里对数组名使用sizeof可以查询到数组实际的长度。
pa 仅仅是一个指向 int 类型的指针编译器根本不知道它指向的是一个整数还是一堆整数。
虽然在这里它指向的是一个数组但数组也只是一块连续的内存没有开始和结束标志也没有额外的信息来记录数组到底多长。
所以对 pa 使用 sizeof 只能求得的是指针变量本身的长度。
也就是说编译器并没有把 pa 和数组关联起来pa 仅仅是一个指针变量不管它指向哪里sizeof求得的永远是它本身所占用的字节数。
2二维数组
大家不要认为二维数组在内存中就是按行、列这样二维存储的实际上不管二维、三维数组... 都是编译器的语法糖。
存储上和一维数组没有本质区别举个例子
int array[3][3] {{1, 23}, {4, 56}{7, 8, 9}};
array[1][1] 5;
或许你以为在内存中 array 数组会像一个二维矩阵:
1 2 3
4 5 6
7 8 9
可实际上它是这样的
1 2 3 4 5 6 7 8 9
和一维数组没有什么区别都是一维线性排列。
当我们像 array[1][1]这样去访问的时候编译器会怎么去计算我们真正所访问元素的地址呢
为了更加通用化假设数组定义是这样的:
int array[n][m]
访问: array[a][b]
那么被访问元素地址的计算方式就是: array (m * a b)
这个就是二维数组在内存中的本质其实和一维数组是一样的只是语法糖包装成一个二维的样子。
6、 void 指针
想必大家一定看到过 void 的这些用法
void func();
int func1(void);
在这些情况下void 表达的意思就是没有返回值或者参数为空。
但是对于 void 型指针却表示通用指针可以用来存放任何数据类型的引用。
下面的例子就 是一个 void 指针
void *ptr;
void 指针最大的用处就是在 C 语言中实现泛型编程因为任何指针都可以被赋给 void 指针void 指针也可以被转换回原来的指针类型 并且这个过程指针实际所指向的地址并不会发生变化。
比如:
int num;
int *pi num;
printf(address of pi: %p\n, pi);
void* pv pi;
pi (int*) pv;
printf(address of pi: %p\n, pi);
这两次输出的值都会是一样: 平常可能很少会这样去转换但是当你用 C 写大型软件或者写一些通用库的时候一定离不开 void 指针这是 C 泛型的基石比如 std 库里的 sort 函数申明是这样的:
void qsort(void *base,int nelem,int width,int (*fcmp)(const void *,const void *));
所有关于具体元素类型的地方全部用 void 代替。
void 还可以用来实现 C 语言中的多态这是一个挺好玩的东西。
不过也有需要注意的不能对 void 指针解引用
比如
int num;
void *pv (void*)num;
*pv 4; // 错误
为什么
因为解引用的本质就是编译器根据指针所指的类型然后从指针所指向的内存连续取 N 个字节然后将这 N 个字节按照指针的类型去解释。
比如 int *型指针那么这里 N 就是 4然后按照 int 的编码方式去解释数字。
但是 void编译器是不知道它到底指向的是 int、double、或者是一个结构体所以编译器没法对 void 型指针解引用。
关于指针想写的内容还有很多这其实也只算是开了个头限于篇幅以后有机会补齐以下内容: 二维数组和二维指针 数组指针和指针数组 指针运算 函数指针 动态内存分配: malloc 和 free 堆、栈 函数参数传递方式 内存泄露 数组退化成指针 const 修饰指针 ...
到此基本上涵盖了 C 语言的基本知识。
点 击 下 面 公 号关 注 “C和C加加”后 回 复“PDF” 即 可 获 取 PDF版 和 详 细 教 程