c语言复习二(基本运算符,指针,数组)

类型转换

  • 概念:不一致但相互兼容的数据类型,在同一表达式中将会发生类型转换。
  • 转换模式:
    • 隐式转换:系统按照隐式规则自动进行的转换,不会造成精度丢失
    • 强制转换:用户显式自定义进行的转换,可能会造成精度丢失
  • 隐式规则:从小类型向大类型转换,目的是保证不丢失表达式中数据的精度

隐式转换是当参与运算的数据类型不一样时,系统自动帮我们进行的类型转换。规则为:

强制转化是按照我们认为所想要进行的运算方式,去手动地对数据进行类型转换,比如下面这个例子:

char  a = 'a';
int   b = 12;
float c = 3.14;
float x = a + b - (int)c; // 在该表达式中a隐式自动转换为int,c被强制转为int

这里强制类型转换改变了系统对这块内存的解释方式,可能会造成精度缺失。

数据类型的本质

  • 概念:各种不同的数据类型,本质上是用户与系统对某一块内存数据的解释方式的约定。

类型转换,实际上是对先前定义时候的约定,做了一个临时的打破。理论上,可以对任意的数据做任意的类型转换,但转换之后的数据解释不一定有意义。

可移植性数据类型

指的是在不同计算机体系结构和操作系统中具有相同大小和语义的数据类型。这些数据类型的定义和行为在不同平台上是一致的,这样可以确保程序在不同系统上编译和执行时具有相同的结果。

在编程中,为了确保程序的可移植性,程序员通常会使用可移植性数据类型,而不是依赖于特定平台的数据类型。例如,在C语言中,为了保证整数类型的大小和行为在不同系统上一致,可以使用 <stdint.h> 头文件中定义的 int8_t, int16_t, int32_t 等固定大小的整数类型,而不是直接使用 intlong 等类型,因为这些类型在不同系统上的大小可能不同。

这样可以确保一个程序在多个不同的系统环境下运行能得到相同的结果

typedef int int32_t;  // 将类型 int 取个别名,称为 int32_t
typedef long int64_t; // 将类型 long 取个别名,称为 int64_t

移位操作的复习

左移操作符 (<<):将操作数的二进制位向左移动指定的位数。左移操作会在右侧补零。例如,对于表达式 a << ba 是被移动的数,b 是移动的位数。

右移操作符 (>>):将操作数的二进制位向右移动指定的位数。右移操作有两种:逻辑右移和算术右移。

  • 逻辑右移:用0填充最高位(左侧)移出的位。适用于无符号数。
  • 算术右移:用符号位填充最高位(左侧)移出的位。适用于有符号数。

c语言中实际开发中我们只会对无符号数进行左移右移,我们不会傻傻的自找麻烦。

复合赋值符

采用复合赋值符不仅直观,且能提高运算效率。主要是补充几个之前没见过的,了解一下。

// 加减乘除:
a += n; // 等价于 a = a+n;
a -= n; // 等价于 a = a-n;
a *= n; // 等价于 a = a*n;
a /= n; // 等价于 a = a/n;

// 求余:
a %= n; // 等价于 a = a%n;

// 位运算:
a &= n; // 等价于 a = a&n;
a |= n; // 等价于 a = a|n;
a ^= n; // 等价于 a = a^n;
a >>= n; // 等价于 a = a>>n;
a <<= n; // 等价于 a = a<<n;

逻辑运算符

  • 在逻辑与运算中,如果左边表达式的值为假,那么右边表达式将不被执行。
  • 在逻辑或运算中,如果左边表达式的值为真,那么右边表达式将不被执行。

switch语句复习

  • 逻辑:根据不同的条件执行不同的代码片段
  • 语法:
switch(n)
{
case 1:
    printf("one\n");
    break;

case 2:
    printf("two\n");
    break;
  
case 3:
    printf("three\n");
    break;
  
default:
    printf("其他数字\n");
}
  • 要点解析:
  1. switch(n) 语句中的 n 必须是一个整型表达式,即 switch 判断的数据必须是整型
  2. case 语句只能带整型常量,包括普通整型或字符,不包括 const 型数据。
  3. break 语句的作用是跳出整个 swtich 结构,没有 break 程序会略过case往下执行
  4. default 语句不是必须的,一般放在最后面(因此不需要 break)

指针复习

可能是一些题外话

变量名称(文件)不需要占用内存。变量名称只在编译期间存在,用于编译器识别和管理变量。在程序执行时,变量名将会被编译器处理为对应的内存地址或寄存器,因此不占用程序空间。变量名字纯纯就是为了我们编写程序的时候更好地操作这块内存来达到我们的目的。

对于指针来说指针名字也不占用空间,是我们的指针变量占用空间。指针变量是用来存放其他(数据的地址)的一种变量。

我们可以将我们的所有内存抽象为一栋楼房,楼房中所有的房间都可以来放置东西,也就是有内存,我们每一个房间都可以用门牌号(地址)来唯一标识,所以每个房间既有内存,又有门牌号。不论是我们的普通变量还是指针变量这些变量我们都可以认为是房间。

变量名称其实可以认为是我们认为地为这些房间赋予了一个人类容易辨识的名字,比如说我们可能将502叫做领导办公室,302叫做杂物间。这样有利于我们站在人的角度来对这些房间进行管理和利用。

每个房间里面可以放的东西不一样,比如说可以放整型数,可以放浮点数,也可以放置一个地址(对应指针变量)。

指针的基础理解

指针??是??
指针是内存中一个最小单元的编号,也就是地址
平时口语中说的指针,通常指的是指针变量,是用来存放内存地址的变量
所以说:指针就是地址,人们口中的指针变量也是指针。

指针变量??是??
我们可以通过&(取地址操作符)取出变量的内存与实地址,把地址可以存放到一个变量中,这个变量就是指针变量。int a=3;int *p=&a;p就是指针变量,保存着一个地址值


上面这种理解主要是对于便于理解而说的,但从严格意义上说,这种说法有些简单化,可能会导致误解。更准确地说,指针是一个变量,其值是另一个变量的地址,指针和地址是由本质化区别的

虽然地址本身没有类型,但通过指针的使用,我们实际上给地址赋予一种“上下文类型”,这有助于正确地处理和解释数据。我们实际上用到地址的时候也会考虑其类型,将其赋值给指针变量的时候也需要考虑类型,可能有时候也需要类型转换。所以我们使用的时候也不能过分地区分他们,就算你简单认为他们等价是没有问题的。我们在实际运用过程,可以直接认为地址也有类型

想要更加详细严格了解指针和地址的不同,请看下面指针的进阶理解。

指针的类型可以理解为由两部分组成

  1. 指针的部分:表示这是一个指针变量。
  2. 指向的数据类型部分:表示指针所指向的数据类型。

可以从两个部分来理解指针的数据类型:

int a=1;

int *p = &a;
  1. 指针本身的类型

指针本身是一种数据类型,(*p)中的(*)表示p这个变量是一个指针变量,用来存储一个地址。这个地址指向另一个变量或内存位置。

     2.指针指向的类型

  • 指针的类型还包括它指向的数据类型,这决定了指针解引用时如何解释所指向的内存中的数据。
  • 例如,int *p 中的int表明p是指向整型数据的指针,float *p 中的float表明p是指向浮点型数据的指针,char **p应该理解为char *(*p)表明p是一个指针,这个指针指向一个char *类型的指针

指针的进阶理解(随便看看这块就行)

  • 指针是一个变量

    • 指针本身是一个变量,它占据内存空间并且有自己的地址。
    • 指针存储的是另一个变量的地址。
  • 指针的类型

    • 指针有类型,如 int *char * 等,这些类型决定了指针指向的变量类型以及指针在内存中的解释方式。
    • 不同类型的指针在解引用(访问指针指向的值)时的行为是不同的。
  • 指针的内存地址

    • 指针变量的值是一个内存地址,即它指向的变量在内存中的位置。
    • 指针变量本身也有一个地址,即指针变量在内存中的位置。
  • 指针和地址的区别

    • 指针保存了一个地址,还包含了类型信息,指示它指向的内存区域的数据类型。
    • 地址只是一个纯粹的内存位置值
    • 比如说对于a[]这个数组来说,首元素的地址==数组a整体的地址,
    • 但是指向首元素的指针!=指向数组a的指针。因为指针还带有它指向的内存区域的数据类型,类型不一样对应指针中的运算也不一样。

我们应该像上面这样来理解指针。虽然地址本身没有类型,但通过指针的使用,我们实际上给地址赋予一种“上下文类型”,这有助于正确地处理和解释数据。我们实际上用到地址的时候也会考虑其类型,将其赋值给指针变量的时候也需要考虑类型,可能有时候也需要类型转换。

所以为了放置搞混,我们就认为地址和指针一样,也有类型。

比如这里虽然&a是个指针但是+1运算仍然使用指针运算规则,并且这里也进行了类型转换,好像看起来并不像个严格意义上的指针。我们不要刻意过分地去区分这两个概念。

指针的大小

指针的大小在32位平台是4个字节,在64位平台是8个字节。

因为64位操作系统中数据的地址(即内存地址)都是64位的,而指针变量保存的就是数据地址所以也为64位。

为什么指针有多种类型??

因为为了指向多种不同类型的变量。

你想变量有着不同的类型,那指针是指向不同类型变量的地址,那么指针也有不同类型听起来是不是没毛病。不同类型的变量的地址就应该放在对应的不同类型指针变量中!!!

好吧,让我们来科学地探究这个问题。以下是几个指针类型不同的重要原因:

1.内存解引用

不同类型的指针在解引用时会告诉编译器如何解释指针所指向的内存内容。例如:

  • int *p 表示 p 指向一个整数,解引用时将内存解释为 int 类型。
  • float *p 表示 p 指向一个浮点数,解引用时将内存解释为 float 类型。

这样才能精确地对到所指向内存进行合适地解释,从而得到正确的数据。

2. 指针运算

指针的类型决定了指针运算(如指针的加减法)的步长。例如:

  • 对于 int *pp + 1 会跳过 sizeof(int) 个字节。
  • 对于 float *pp + 1 会跳过 sizeof(float) 个字节。

这是因为不同的数据类型在内存中占用的空间不同,指针运算需要知道具体的数据类型以便正确地进行内存地址的计算。如果不这样搞的话,比如说对于数组,我们知道数组名本质上就是一个指针,下标访问数组中不同元素的内部实现 其实就是指针的偏移

如果我们不将指针指向数据元素的类型作为指针类型的一部分,那么指针每次偏移的时候不知道向后移动多少个字节,怎么能精确到达下一个数组元素的位置来获取对应的值呢?

指针中的运算

指针运算是计算机编程中的一个重要概念,特别是在使用C或C++等低级语言时。指针运算允许程序员直接操作内存地址。以下是一些基本的指针运算:

  1. 指针加减常数:将一个整数加到指针上,指针会向前移动该整数乘以所指向数据类型的大小。例如,如果有一个int类型的数组,int *p = arr;,那么p + 1将会指向数组的下一个元素。举个例子吧:p此时指向(保存)的地址是0x123,那么p+2指向的地址就是0x12B,因为偏移了8个字节,(int)p+2指向的地址就是0x125,因为强制转换为int就是使用int数据类型中的计算方法,直接加2和指针的类型无关了。

  2. 指针相减:两个指针相减得到的是它们之间的元素数量,而不是地址差。例如,如果p1p2都指向同一个数组,p2 - p1将给出p2p1之后的元素数量。

这里有几个关键点需要注意:

  1. 同类型指针:进行指针相减操作的两个指针必须是同类型的,因为它们表示的是相同数据类型元素的地址。

  2. 指向相同数据结构:这两个指针通常指向同一个数组或连续内存块中的元素。如果两个指针指向不同的数组或非连续的内存区域,它们之间的差值就没有实际意义,并且试图进行这种操作可能会导致未定义行为或程序错误。

       3. 指针比较:可以比较两个指针是否相等或不等,以及它们的大小关系。例如,p1 == p2

          查两个指针是否指向同一个地址。

地址运算:使用&操作符可以由变量获取到变量的地址,然后这个地址可以赋给一个指针

解引用运算:使用*操作符可以获取地址所对应存储的值,使用*操作符可以访问指针指向的值,例如 *p 将给出p所指向的值。

指向数组单个元素的指针和指向整个数组的指针

前言:用不同方式来访问更改变量a存储的值。下面是具体示例:

如何判断一个变量的类型?

对于一个变量来说去掉变量名称剩余的是不是就是我们变量的类型,比如说int a[3],int [3]就是a的类型。int[3] 描述了这个数组的类型:一个包含3个 int 类型元素的数组。

  • 普通类型:只需要指定变量名和类型,例如 int number;
  • 数组类型:需要指定变量名、类型和元素数量,例如 int numbers[10];

如何定义指向整个数组的指针和数组中某个元素的指针?(定义指针的步骤)

指向整个数组的指针 :

int b[3]

1.说到地址,不用想,肯定是用指针变量来保存,所以肯定是(*p)

2.然后就是我们要保证指针指向的数据类型和指针可以指向的数据类型相匹配。也就是目标的类型和我们指针可以指向的目标类型必须匹配。 (指针的类型必须与它所指向的数据类型匹配。)

那目标类型是什么呢?肯定是数组类型了,因为这里是要指向整个数组。那这个数组类型应该如何表示呢?上面也说过了去掉变量名称剩下的就是变量的类型,所以这里变量b的类型就是int [3]。

综合下来就是 int [3] (*p),但是还是要注意一点,数组类型的话分为两部分,我们要将变量放在中间,这是我们的规定,所以最终结果就是int (*p)[3]=&b;这样便定义了一个指向整个数组地址的指针。3.这里肯定对于b是要&的,因为int [3]和b的类型(int [3])相互匹配,指向的是这个变量的地址,后面肯定要再加上一个取值符号。搞不懂这点的话举个很简单的例子试试

比如int a=3;int *p=a;这肯定是错的

指向数组中某个元素的指针 :

int myArray[10];

int *p = &myArray[0];表示的是定义了一个指向数组第一个元素的指针

int *p = myArray; //和上面得到的结果一样,只不过这里是选择了将myArray这个指针直接赋值给了p这个指针变量。myArray这个指针也是指向数组第一个元素。这里并不是用到上面对变量取地址的方法,而是直接指针给指针赋值。

k和&b就是等价的,*k和b也是等价的。可以任意相互替换使用

接下来请秒杀掉下面这个指针的定义和使用。这是个函数的指针。

辨析数组的指针和指针型数组

数组的指针上面我们已经讲到过了:int (*p)[3]=&b;

这里 p 是一个指针,指向一个包含3个整数的数组。这种语法 int(*p)[3] 表示 p 是一个指向整型数组的指针,数组中有3个元素。p是一个数组的指针

如果将小括号去掉,int *p[3]=&b;

  • int *p[3] 表示 p 是一个数组,包含3个元素,每个元素是指向整数的指针。
  • &b 是数组 b 的地址,但在这种语境下,&b 的类型是 int (*)[3],而不是 int *,因此类型不匹配,会导致编译错误或警告。

这种现象的原因是优先结合的问题,c语言中,[]的优先级高于*, 当不加小括号的时候p会和[]优先结合,从此过上了幸福美好的生活,p就成为一个数组,数组中的每个元素类型是int *类型,都是int型指针。

总结:

当你声明一个指针变量时,推荐使用小括号将 * 和变量名括起来,这样做可以提高代码的可读性和清晰度。使用小括号有助于明确指针的类型和它所指向的目标类型,尤其是在复杂的类型声明中。

比如更改上面我们想要声明一个指向数组的指针变量,因为是一个指针,我们就应该(*p)这样不容易出错,同时这样也能更好地帮助我们理解,一眼就能看出这是个指针变量,对于很简单的指针变量我们可以不加()。

例子:int *(*p)[10]; // p 是一个指向包含 10 个 int 指针的数组的指针。

通过加()这种方式,可以更明确地理解指针的类型和它所指向的目标类型。

野指针

野指针是什么??

野指针是指指向未知内存位置或者已经释放内存的指针。

 什么情况会造成野指针??

引用未初始化的指针、访问已释放内存、数组边界越界等行为都可能导致野指针。

野指针的危害

  1. 引用野指针,相当于访问了非法的内存,常常会导致段错误(segmentation fault)
  2. 引用野指针,可能会破坏系统的关键数据,导致系统崩溃等严重后果

很多情况下,我们不可避免地会遇到野指针,比如刚定义的指针无法立即为其分配一块恰当的内存,又或者指针所指向的内存被释放了等等。一般的做法就是将这些危险的野指针指向一块确定的内存,比如零地址内存。

如何能够防止野指针的出现??

  1. 指针初始化
  2. 小心指针越界
  3. 指针指向空间释放,及时置NULL
  4. 避免返回局部变量的地址
  5. 指针使用之前检查有效性
// 1,刚定义的指针,让其指向零地址以确保安全:
char *p1 = NULL;
int  *p2 = NULL;

// 2,被释放了内存的指针,让其指向零地址以确保安全:
char *p3 = malloc(100); // a. 让 p3 指向一块大小为100个字节的内存
free(p3);               // b. 释放这块内存,此时 p3 相当于指向了一块非法内存
p3 = NULL;              // c. 让 p3 指向零地址

我们利用free将p3指向的那一块内存归还给系统之后,p3指针仍然指向着这块内存,就是说你仍然可以利用这个指针来访问修改这块内存中的数据,这是非常危险的一件事情,所以在内存回收之后,我们应该即使地将这个指针置空,即指向0地址内存,0地址内存是不允许修改的,所以之后用到这个指针也不会造成任何影响。 

数组的复习

神奇的[ ]

定义数组时的 []

  • 在定义数组时,[] 用于指定数组的大小或者留空以让编译器根据初始化列表自动确定数组的大小。

使用数组元素时的 []

  • 在使用数组时,[] 用于访问数组中特定索引位置的元素。

所以这两处地方的[ ]含义不一样。

多维数组的元素类型

 二维数组中的a[2]表示a数组申请了两个空间,每个空间存放的数据类型是int [3](占用3个int大小)

a[0],a[1]都表示着一个一维数组名。

数组名含义(重要)

虽然数组名本身不会改变其类型,但它在不同的情况下会被视为不同的类型。

  • 在C语言程序中,数组的出现有两种可能的含义:

    1. 代表整个数组
    2. 代表其首元素的地址(指针类型)
  • 当出现以下情形时,数组代表的是整个数组

    1. 在数组定义中
    2. 在 sizeof 运算表达式中
    3. 在取址符&中,此时&a便代表着整个一维数组的地址,虽然说整个数组的地址和首元素的地址看起来值是一样的,但是这里表示的是整个数组的地址,而不是首元素的地址,运算起来是不一样的。&a+1便跳过了整个数组,越界了,就变成了野指针。(因为这个指针指向的数据类型是数组类型)
  • 在这些情况下,数组可以被看作是一个整体的实体,类似于结构体。这种视角对于传递数组给函数、计算数组的长度、或者获取数组的地址都非常有用。
  • 当出现其他情形时,数组名都代表其首元素地址 。比如说在函数传递时传递的是数组名字,实际上是以指针的形式传递的,就是传递了一个地址,也就是首元素的地址。a[1]=1;此时a也代表的是首元素地址。

  • 数组下标实际上是编译系统的一种简写,其等价形式是:
a[i] = 100;  等价于  *(a+i) = 100;

因此,不能武断地说数组是不是代表地址,而要看它出现的场合!!!

数组名其实是一个常量指针,指向数组的第一个元素,其值在数组的生命周期内是固定的。永远保存着数组中第一个元素的地址。数组名在许多上下文中会隐式地转换为指向数组第一个元素的指针。例如,当数组名被用作函数参数时,传递了指针的值即首元素地址。

下面我将使用一个例子来带大家感受数组名在不同情况下,作为不同类型下的使用。

这是一段将小写字母转换为大写字母的代码,但是这个代码中有一处错误

#include <stdio.h>
#include <limits.h>
#include <ctype.h>

void upper_case(char str[])
{
    int step = 'a' - 'A';
    for(int i = 0; i<sizeof(str)/sizeof(str[0]); i++)
    {
        if(islower(str[i]))
            str[i] -= step;
    }
}
int main(void)
{
    char str[] = "abcdefghijklnmopqrstuvwxyz";
    printf("原数组:%s\n", str);
    upper_case(str);
    printf("转换后:%s\n", str);
}

这个错误就是下面这里,这里其实是得不到正确的str数组中的字符数的。

 i<sizeof(str)/sizeof(str[0])

这里的sizeof(str)得到的是str指针的大小,得到的并不是数组的大小。由于在 C 语言中,当数组名作为函数参数传递时,数组名会完全被转换为指向数组首元素的指针。这种转换丢失了一部分数组原本的性质,传递过去的就是一个地址。因为因此,sizeof(str)upper_case 函数中实际上是指针的大小,而不是数组的大小。

我们可以通过如下方法来解决:

在 C 语言中传递数组时,数组名会被转换为指向数组首元素的指针。因此,不能在函数内部通过 sizeof 操作符获取数组的大小。可以通过遍历到字符串的终止符 \0 来解决这个问题,或者在函数调用时显式传递数组的长度。(增加一个参数)

  • 22
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值