C语言入门基础(二)

news2024/11/23 9:20:05

基本概念

地址

计算机的内存是一块用于存储数据的空间,由一系列连续的存储单元组成,就像下面这样,
在这里插入图片描述
每一个单元格都表示1个Bit,一个bit在EE专业的同学看来就是高低电位,而在CS同学看来就是0,1两种状态。
由于1个bit只能表示两个状态,所以大佬们规定8个bit为一组,命名位byte
并且将byte作为内存寻址的最小单元,也就是给每个byte一个编号,这个编号就叫内存的地址
在这里插入图片描述

变量的本质

接下来我们需要考虑,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、指针、数组、结构体、对象…都是这样放在内存的。

指针

指针是什么?

变量放在哪?通过上面的讲述可知,定义一个变量实际就是向计算机申请了一块内存来存放。
那如果我们向知道变量到底存放在哪,可以通过运算符&来取得变量实际的地址,这个值就是变量所占内存块的起始地址
ps:实际上这个地址是虚拟地址,并不是真正物理内存上的地址。
我们可以把这个地址打印出来
printf(“%x”,&a);
大概会是这样的一串数字:0x7ffcad3b8f3c
上面说,我们可以通过&符号获取变量的内存地址,那获取之后如何来表示这是一个地址,而不是一个普通的值呢?也就是在C语言中如何表示地址这个概念呢?
对,就是指针,你可以这样 int *pa = &a;
pa中存储的就是变量a的地址,也叫做指向a的指针。
在这里谈几个看起来有点无聊的话题:

1、为什么我们需要指针?直接用变量名不行吗?

当然可以,但是变量名是有局限的。

2、变量名的本质是什么?

是变量地址的符号化,变量名是为了让我们编程时更加方便,对人友好,可计算机不认识什么变量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型变量的地址。
编译器也可以通过类型检查来排除一些编译错误。
这就是指针存在的必要性。
实际上任何语言都有这个需求,只不过很多语言为了安全性,给指针戴上一层枷锁,将指针包装成了引用。

指针的本质都是变量的内存首地址,指针是用来存放地址的,地址是唯一标识一块地址空间的。指针的大小在32位平台是4个字节,在64位平台是8个字节。
那么为什么还要有各种类型呢?
比如int指针,float指针,这个类型影响了指针本身存储的信息吗?这个类型会在什么时候发挥作用?

指针变量

指针变量是用来保存变量地址的一种变量,那该如何定义呢?如何让计算机一眼认出来这一串数字是地址,而不是普通的数字呢

int a = 10;
int* pa = &a;  创建指针变量pa,pa存放整型a的地址
 
char b = 'h';
char* pb = &b; 创建指针变量pb,pb存放字符型b的地址

指针变量的定义方式是 基本类型 指针变量名 = &变量名;
int
类型的指针存放int类型变量的地址
char*类型的指针存放char类型变量的地址
pa和pb的指针类型不同("int *和char * "),这是定义指针时决定的,那么这两种类型的指针还有什么本质上的区别吗?

指针变量的类型有两个意义:

1、指针类型决定了指针变量访问空间的能力
2、指针类型决定了指针的步长,即指针变量加或减一个整数,指针向后或向前移动的字节数。
具体可参考:添加链接描述

指针的操作

  • &
    这个符号为取值运算符

&变量名表示取变量的地址,就是获取变量的指针

int a = 123;
int* p = &a; //取变量a的地址赋值给指针变量p

这个是指针运算符(或称"间接访问"运算符)

*指针变量表示取指向的变量的值

int a = 123;
int* p = &a; //取变量a的地址赋值给指针变量p
printf("%d",*p); //输出123,*p表示取a的值

野指针

先看下面代码

#include <stdio.h>
int main(void){
	int *p;
	*p = 10;
	return 0;
}

这里我们首先定义了一个int类型的指针p,当然,这个p里面保存的是个地址,且这个地址所对应的存储空间只能存int数据,所以叫做int类型的指针。
注意
我们是没有个这个p初始化的,所以,开始p里面存的可能是任何一个数,也就是任何一个地址,这个地址,我们有可能有权限访问,也有可能没有权限访问,这个是我们无法控制的。
然后,将这个地址上存的内容取出来*p,再对这个内容进行修改,这样能成功吗?大概率是不能成功的。
因为我们没有初始化p,所以,如果p中开始给随机分配了一个我们没有权限访问的地址,然后,后面我们还要对这个位置的内容进行修改,那就肯定会报段错误。
所以,指针,一定要记得初始化。
这种,没有初始化的指针,就可以称为:野指针

注意:指针定义的关键:1、最好定义的时候就对指针进行赋值 2、int类型的指针只能存int类型的变量的地址,char类型的指针只能存char类型的地址,要对应起来

如何规避野指针。请参考添加链接描述 该片博客里面由详细介绍。我也是参考这里。

指针与数组

一维数组,二维数组,字符数组的定义,引用初始化的相关介绍请参考:添加链接描述

指针与数组氛围两个方面:

一、指向数组元素的指针
二、指针数组(数组元素是指针)

一、指向数组元素的指针

数组元素地址赋值给指针变量

在C语言中,数组名就是这个数组下标为0的数组元素地址。这个地址也是这个数组的地址。
所以数组给指针赋值和数组元素给指针赋值很不一样。因为,指针只能存地址。数组名本身就保存的地址,就直接可以给指针变量赋值。数组的元素保存的就是具体的内容了,可能是int也可能是char,所以数组元素赋值的时候,就要先用取地址符&得到这个元素的地址,然后再给指针变量赋值。

int c[10], d[20];
int *p, *q=&c[0];

p = &c[3];//数组c下标为3的元素赋值给指针P
p = d;//整个数组的地址赋值给指针p
指针运算

众所周知,数组是存在内存中一片连续的区域中。即,数组在逻辑上是一个元素挨着一个元素,在物理存储上也是一个元素挨着一个元素的。
所以当指针变量指向数组元素时,指针变量加/减一个整数n,表示指针向后/前移动n个元素

二、指针数组

在这里要先了解一下C语言中的运算符优先级。
在这里插入图片描述
这里还要重点区分两个概念:一个是指针数组,一个是数组指针。先来看两个例子。

int i = 1;
int j = 2;
int k = 3;
int l = 4;
int *p[4] = {&i, &j, &k, Kl};//单目运算符,同优先级的情况下,丛右往左进行运算
							//[]先与p结合,表示这是一个数组
							//*再与p[]结合,表示这个数组元素类型是指针
							//最后加上前面的int,表示这个int类型的指针
int (*p)[5];
//这里*先与p结合,表示p是一个指针,这个指针指向这个数组,这个数组是int类型的数组
int *p[5];//这个表示指针数组,每个元素都是指针,上面说过了

指针数组:是先数组后指针,本质是一个数组,数组中每一个元素都是指针。格式:数据类型 * 指针数组名[下标]
数组指针:是先指针后数组,本质是一个指针,用来指向二维数组的,也叫做行指针。多用于二维数组作为函数的参数传递时. 格式:数据类型(*数组指针名)[列宽]

int s[3][4] = {{1,2,3,4},
                {5,6,7,8},
                {9,10,11,12}};    //定义了一个 3行4列的二维数组
int (*p)[4] = s;    //定义了个数组指针 并让这个指针指向二维数组s

指向二维数组的指针保存的是二维数组的首地址,p是一个变量。
这样我们就可以得到–>
在这里插入图片描述

字符串的指针

在C语言中是没有字符串类型的,C语言中的字符串都是用字符数组进行存储的,字符串的滋镇就是字符数组的指针,也就是字符数组得首地址

C语言中字符串的两种定义形式:

  • 数组形式:char string[] ={‘h’,‘e’,‘l’,‘l’,‘o’,‘\0’};或char string[]=“hello”;
  • 指针形式:char* string=“hello”;(等价于{‘h’,‘e’,‘l’,‘l’,‘o’,‘\0’})

二级指针

二级指针是用来保存一级指针的地址的。
多用于一级指针的地址作为函数的参数传递的。
在这里插入图片描述

指针与函数

指针函数

指针函数:
函数的返回值的类型既可以是整型(若没有设定,则默认为整型),实型,字符型,也可以是指针型。返回值为指针类型的函数又称为指针型的函数,则为指针函数。指针函数的本质是一个函数,返回值是一个指针类型。

格式:

返回值类型 * 函数指针名(函数形参表);

注意:
1、不能返回局部变量的地址。(局部变量的地址在函数结束时会被系统回收)
2、可以返回全局变量的地址
3、可以返回static关键字修饰的局部变量的地址
4、可以返回传递给函数的参数的地址。(函数形参表中传入的地址)
例子:

int *my_func(int x,int y){
    int sum = x + y;
    return &sum;    //错误的,sum是局部变量 在函数结束时会被回收
}
 
int *my_func(int x, int y,int *sum){
    *sum = x + y;
    return sum;    //正确的
}

函数指针

C语言的每个函数在编译的时候,都分配了一段连续的内存空间和一个入口地址,这个入口地址就称为"指向函数的指针"
即函数指针。可以用一个变量来存储函数指针,这个变量就称为"指向函数的指针变量"或者函数指针变量。通过函数指针变量就可以调用所指向的函数,改变它的值就可以动态调用不同的函数。函数指针的本质是一个指针,可以指向一个函数
格式:
返回值类型(*函数指针名)(函数的形参表);
例子:

#include <stdio.h>
 
int my_add(int x, int y){
    return x+y;
}
 
int main(int argc, const char *argv[])
{
    int a = 10;
	int b = 20;
	printf("%d\n", my_add(a, b));//30
 
	int (*p)(int, int) = NULL;
	//定义了一个函数指针 指针名叫p 可以指向一个 返回值为int
	//形参列表为(int, int)类型的函数
 
	p = my_add;    //让函数指针p指向函数my_add 
                    //函数名就是函数的首地址
 
	//指针指向函数之后 就可以通过指针来调用函数了
	printf("%d\n", p(a, b));//30
 
	return 0;
}

注意:函数指针变量定义时,并不指向哪一个具体的函数,而是指向空指针。
那如何通过函数指针变量调用函数呢?

那就是让函数指针变量p指向一个具体的函数,即将某个函数的入口地址赋值给这个指针变量。
那一般函数的入口地址在哪里呢?
函数名就代表函数的入口地址,是函数指针类型的符号常量。(这里和数组有点类似,数组名也是数组的地址。)

//比如现在有个函数,函数名叫abc,就可以对函数指针变量p赋值了
p = abc;

那赋值给函数指针变量p以后,如何使用p来调用这个函数呢?

还记得我们的取值运算符吗?
本来调用函数应该是这个的
函数名();
那现在函数名的位置用
函数指针变量名来代替就可以啦
剩下的就和普通函数的使用方式基本一样啦。
举个例子
这是linux多线程编程中,创建线程的函数,这里看他的倒数第二个参数,根据上面的知识点返回值是指针的函数和指向函数的指针,来分析一下这个参数要传入的是什么?

#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
				  void *(*start_routine)(void *), void *arg);
//倒数第二个参数
//void * (函数名)(void*) 这个函数返回值是一个指针,函数的参数是void *
//void (*指针名)(void*) 这是一个指针,这个指针指向一个函数,函数的返回值为void,参数为void*
//void *(*指针名)(void*) 
//这是一个指针,这个指针指向一个函数,函数的返回值为void*,函数的参数为void
//所以,对于这个参数,我们要先定义一个函数,这个函数的返回值是void*
//这个函数的参数是void*。然后,将这个函数的函数名传入进来。

函数指针的经典使用场景---->回调函数
例子:

#include <stdio.h>
 
int my_add(int x, int y){    //函数my_add
    return x+y;
}
 
int my_sub(int x, int y){    //函数mu_sub
    return x-y;
}
 
int jisuan(int x, int y, int (*p)(int, int)){    //第三个参数就是一个函数指针
	return p(x, y);    //调用函数指针p指向的函数(回调)
                        //通过传递不同的函数实现不同的功能
}
 
int main(int argc, const char *argv[])
{
	int a = 10;
	int b = 20;
	printf("a+b = %d\n", jisuan(a, b, my_add));//调用函数my_add
	printf("a+b = %d\n", jisuan(a, b, my_sub));//调用函数my_sub
 
	return 0;
}

函数指针数组

本质是一个数组,数组中每个元素都是一个函数指针。
格式:

返回值类型(*函数指针名下标)

例子:

#include <stdio.h>
 
int my_add(int x, int y){
    return x+y;
}
 
int my_sub(int x, int y){
    return x-y;
}
 
int main(int argc, const char *argv[])
{
	int (*s[2])(int, int) = {NULL};    //定义了一个函数指针数组,数组名叫s 数组中共有2个元素
	                                   //每个元素都是一个可以指向返回值为int
	                                    //形参列表为 (int, int) 的函数指针
	s[0] = my_add;    //将每个元素指向函数
	s[1] = my_sub;     
	               
 
	//当函数指针数组的元素指向函数之后 就可以通过他调用函数了
	int a = 10;
	int b = 20;
	printf("a+b = %d\n", s[0](a, b));//通过函数指针数组中的元素调用函数
	printf("a-b = %d\n", s[1](a, b));
	
	return 0;
}

函数指针数组指针

本质是一个指针,指向一个函数指针数组
格式:
返回值类型(*(*函数指针数组指针名))(函数的形参表)
例子:

#include <stdio.h>
 
int my_add(int x, int y){
    return x+y;
}
 
int my_sub(int x, int y){
    return x-y;
}
 
int main(int argc, const char *argv[])
{
 
	int (*s[2])(int, int) = {my_add, my_sub};//定义了一个函数指针数组,
                                             //数组名叫s 数组中共有2个元素
	                                         //每个元素都是一个可以指向返回值为int 
	                                         //形参列表为 (int, int) 的函数指针
	                                         //s[0]指向函数my_add;s[1]指向函数my_sub
	int a = 10;
	int b = 20;
	printf("a+b = %d\n", s[0](a, b));//30
	printf("a-b = %d\n", s[1](a, b));//-10
 
	
	int (*(*p))(int, int) = NULL;    //定义了一个 函数指针数组指针 
                                        
	p = s;    //让函 数指针数组指针 p 保存 函数指针数组 的首地址
	
	printf("a+b = %d\n", p[0](a, b));//通过指针就可以访问函数指针数组的元素了
	printf("a-b = %d\n", (*(p+1))(a, b));
	
	return 0;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1317141.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

K8s中pod詳解

目录 Yaml语法解析 Pod pod是如何被创建的 1.创建一个pod 2.创建一个多容器pod 进入容器 3.配置节点标签 4.Pod容器的交互 4.1创建pod&#xff0c;并做本地解析 4.2pod共享进程 4.3pod共享宿主机namespace 5.钩子函数lifecycle 基础指令 # 查看对应资源: 状态 $ kubectl…

人工智能_机器学习065_SVM支持向量机KKT条件_深度理解KKT条件下的损失函数求解过程_公式详细推导---人工智能工作笔记0105

之前我们已经说了KKT条件,其实就是用来解决 如何实现对,不等式条件下的,目标函数的求解问题,之前我们说的拉格朗日乘数法,是用来对 等式条件下的目标函数进行求解. KKT条件是这样做的,添加了一个阿尔法平方对吧,这个阿尔法平方肯定是大于0的,那么 可以结合下面的文章去看,也…

首字母转大写在线工具

具体请前往&#xff1a;在线首字母转大写

结构体基础全家桶(2)结构体指针

目录 指向结构体类型数据的指针&#xff1a; 指向结构体变量的指针&#xff1a; 创建&#xff1a; 应用&#xff1a; 注意事项&#xff1a; 指向结构体数组的指针 创建&#xff1a; 应用&#xff1a; 注意&#xff1a; 用结构体变量和指向结构体的指针做函数的参数 …

oracle详细安装教程(附带百度网盘资源)

一,下载安装包途径 1.官网 Unauthorized Request 2.百度网盘分析 https://pan.baidu.com/s/1n221gdTK0Fcho839oRab9g 提取码1q2w 二&#xff0c;安装教程 1.下载完安装包后点击 setup.exe 如果出现一下的问题&#xff0c;使用windows10等系统安装oracle 11g等版本的数据库…

大 O 表示法在机器学习中的重要性

一、介绍 在不断发展的机器学习领域&#xff0c;算法的效率至关重要。大 O 表示法成为这方面的一个关键工具&#xff0c;它提供了一种描述算法性能或复杂性的语言&#xff0c;特别是在时间和空间方面。本文探讨了 Big O 表示法在机器学习中的重要性&#xff0c;阐明了它在算法选…

Next.js加载异步组件 骨架屏

Next.js 中有两种处理页面加载的方式&#xff0c;一种是 Loading UI 一种是 Streaming。接下来我将介绍这两种的区别&#xff0c;以及实际的业务场景。 当我们进入某个页面时&#xff0c;需要获取页面数据&#xff0c;可能是从数据库读取也有可能是 API 服务&#xff0c;总之这…

【深度学习】注意力机制(六)

本文介绍一些注意力机制的实现&#xff0c;包括MobileVITv1/MobileVITv2/DAT/CrossFormer/MOA。 【深度学习】注意力机制&#xff08;一&#xff09; 【深度学习】注意力机制&#xff08;二&#xff09; 【深度学习】注意力机制&#xff08;三&#xff09; 【深度学习】注意…

【从零开始学习JVM | 第九篇】了解 常见垃圾回收器

前言&#xff1a; 垃圾回收器&#xff08;Garbage Collector&#xff09;是现代编程语言中的一项重要技术&#xff0c;它提供了自动内存管理的机制&#xff0c;极大地简化了开发人员对内存分配和释放的繁琐工作。通过垃圾回收器&#xff0c;我们能够更高效地利用计算机的内存资…

MetaAI发布Seamless:两秒内实现跨语言同声传译

在当今日益互联的世界中&#xff0c;语言差异常常成为沟通的障碍。MetaAI最新发布的语音翻译大模型Seamless&#xff0c;正是为打破这一障碍而生。Seamless不仅提供流畅、高效的多语言翻译功能&#xff0c;更在保留说话人韵律和风格方面取得突破&#xff0c;是AI同声传译领域的…

Python开源项目周排行 2023年第40周

Python 趋势周报&#xff0c;按周浏览往期 GitHub,Gitee 等最热门的Python开源项目&#xff0c;入选的项目主要参考GitHub Trending,部分参考了Gitee和其他。排名不分先后&#xff0c;都是当周相对热门的项目。 入选公式&#xff1d;70%GitHub Trending20%Gitee10%其他 关注微…

Tekton 构建容器镜像

Tekton 构建容器镜像 介绍如何使用 Tektonhub 官方 kaniko task 构建docker镜像&#xff0c;并推送到远程dockerhub镜像仓库。 kaniko task yaml文件下载地址&#xff1a;https://hub.tekton.dev/tekton/task/kaniko 查看kaniko task yaml内容&#xff1a; 点击Install&…

QQ邮箱发送工具类的实现

我们在日常开发中&#xff0c;需要实现一个对邮箱的发送&#xff0c;今天就实现邮箱的发送工具类&#xff0c;只需要一些注册邮箱之后的配置即可&#xff0c;我这边使用的是qq邮箱 0.加上依赖 <!--邮箱--><dependency><groupId>org.springframework.boot&l…

Docker单机部署OceanBase

文章目录 说明机器软硬件要求指导文档本次部署环境说明 OceanBase单机部署&#xff08;Docker&#xff09;一&#xff1a;拉取 OceanBase 数据库相关镜像二&#xff1a;启动 OceanBase 数据库实例完整启动日志展示 三&#xff1a;连接实例遇到报错&#xff1a;没有mysql客户端 …

【lesson14】MySQL表的基本查询retrieve(读取)1

文章目录 表的基本操作介绍retrieveselect列建表基本测试 where子句建表基本测试 表的基本操作介绍 CRUD : Create(创建), Retrieve(读取)&#xff0c;Update(更新)&#xff0c;Delete&#xff08;删除&#xff09; retrieve select列 建表 基本测试 插入数据 全列查询 …

GoogLeNet(pytorch)

亮点与创新&#xff1a; 1. 引入Inception基础结构 2. 引入PW维度变换卷积&#xff0c;启迪后续参数量的优化 3. 丢弃全连接层&#xff0c;使用平均池化层&#xff08;大大减少模型参数&#xff09; 4. 添加两个辅助分类器帮助训练&#xff08;避免梯度消失&#xff0c;用于…

智能电气柜环境监测系统

智能电气柜环境监控系统是一种基于传感器技术和物联网技术的智能化监控系统&#xff0c;用于对电气柜内的环境参数进行实时监测和管理。依托智慧电力运维工具-电易云&#xff0c;通过安装在电气柜内的多个传感器&#xff0c;实时采集电气柜内的温度、湿度、氧气浓度、烟雾等关键…

windows redis 允许远程访问配置

安装好windows版本的redis&#xff0c;会以服务方式启动&#xff0c;但是不能远程访问&#xff0c;这个时候需要修改配置。redis安装路径下会有2个配置文件&#xff0c;究竟需要怎么修改才能生效呢&#xff1f;看下图 这里的redis服务指定了是redis.windows-service.conf文件&…

java_web_电商项目

java_web_电商项目 1.登录界面2.注册界面3. 主界面4.分页界面5.商品详情界面6. 购物车界面7.确认订单界面8.个人中心界面9.收货地址界面10.用户信息界面11.用户余额充值界面12.后台首页13.后台商品增加14.后台用户增加15.用户管理16.源码分享1.登录页面的源码2.我们的主界面 1.…