C语言学习笔记,学懂C语言,看这篇就够了!(中)

附上视频链接:X站的C语言教程

目录

    • 第8章、函数
      • 8.1 函数是什么
      • 8.2 函数的分类
        • 8.2.1 库函数
          • 8.2.1.1 如何使用库函数
        • 8.2.2 自定义函数
      • 8.3 函数参数
        • 8.3.1 实际参数(实参)
        • 8.3.2 形式参数(形参)
      • 8.4 函数调用
        • 8.4.1 传值调用
        • 8.4.2 传址调用
        • 8.4.3 练习
      • 8.5 函数的嵌套调用和链式访问
        • 8.5.1 嵌套调用
        • 8.5.2 链式访问
      • 8.6 函数的声明和定义
        • 8.6.1 函数声明
        • 8.6.2 函数定义
      • 8.7 函数递归
        • 8.7.1 什么是递归
        • 8.7.2 递归的必要条件
        • 8.7.3 练习
        • 8.7.4 递归与迭代
    • 第9章、数组
      • 9.1 一维数组的创建和初始化
        • 9.1.1 数组的创建
        • 9.1.2 数组的初始化
      • 9.2 一维数组的使用
      • 9.3 一维数组在内存中的存储
      • 9.4 二维数组的创建和初始化
        • 9.4.1 二维数组的创建
        • 9.4.2 二维数组的初始化
      • 9.5 二维数组的使用
      • 9.6 二维数组在内存中的存储
      • 9.7 数组越界
      • 9.8 数组作为函数参数
        • 9.8.1 冒泡排序
        • 9.8.2 冒泡排序函数的错误设计
        • 9.8.3 数组名是什么
        • 9.8.4 冒泡排序函数的正确设计
      • 9.9 数据实例
          • 9.9.1数组的应用实例1:三子棋
          • 9.9.2 数组的应用实例2:扫雷游戏
    • 第10章、操作符
      • 10.1 操作符分类
      • 10.2 算术操作符
      • 10.3 移位操作符
        • 10.3.1 左移操作符
        • 10.3.2 右移操作符
      • 10.4 位操作符
      • 10.5 赋值操作符
        • 10.5.1 复合赋值符
      • 10.6 单目操作符
        • 10.6.1 单目操作符介绍
        • 10.6.2 sizeof和数组
      • 10.7 关系操作符
      • 10.8 逻辑操作符
      • 10.9 条件操作符
      • 10.10 逗号表达式
      • 10.11 下标引用、函数调用和结构成员
      • 10.12 表达式求值
        • 10.12.1 隐式类型转换
        • 10.12.2 算数运算
        • 10.12.3 操作符的属性
    • 第11章、常见关键字
      • **11.1** **关键字** **typedef**
      • **11.2** **关键字static**
        • **11.2.1** **修饰局部变量**
        • **11.2.2** **修饰全局变量**
        • **11.2.3** **修饰函数**
    • 第12章、#define 定义 常量 和 宏
    • 第13章、指针
      • 13.1 指针是什么
        • 13.1.1 指针变量
        • 13.1.2 指针变量的大小
      • 13.2 指针和指针类型
        • 13.2.1 指针 +-整数
        • 13.2.2 指针的解引用
        • 13.2.3 指针 +- 整数和指针和的解引用练习
      • 13.3 野指针
        • 13.3.1 野指针成因
        • 13.3.2 如何规避野指针
      • 13.4 指针运算
        • 13.4.1 指针+-整数
        • 13.4.2 指针-指针
        • 13.4.3 指针的关系运算
      • 13.5 NULL指针
      • 13.6 指针和数组
      • 13.7 二级指针
      • 13.8 指针数组
    • 第14章、结构体
      • 14.1 结构体类型的声明
        • 14.1.1 结构的基础知识
        • 14.2.2 结构的声明
        • 14.1.3 结构成员的类型
      • 14.2 结构体的定义和初始化
      • 14.3 结构体成员访问
      • 14.4 结构体传参

第8章、函数

//两数相加
#include <stdio.h>
int main()
{
    intnum1 = 0;
    intnum2 = 0;
    intsum = 0;
    printf("输入两个操作数:>");
    scanf("%d %d", &num1, &num2);
    sum = num1 + num2;
    printf("sum = %d\n", sum);
    return 0;
}
//上述代码,写成函数如下:
//函数:
//f(x) = x + 1
//f(x, y) = x + y
#include <stdio.h>
int Add(int x, int y)//接收   Add--函数名   int 函数的放回类型  ()里面是函数的参数,形参
{//函数体
    int z = x + y;
    return z;
    //把 z 返回去了,返回类型是 int,所以上面接收时要用 int 类型接收
}
int main()
{
    int num1 = 0;
    int num2 = 0;
    int sum = 0;
    printf("输入两个操作数:>");
    scanf("%d %d", &num1, &num2);
    sum = Add(num1, num2);//调用函数, 把实参传给实参,让实参接收,再返回 z 给 sum
    sum = Add(2, 3);//或者直接调用
    printf("sum = %d\n", sum);
    return 0;
}

8.1 函数是什么

数学中我们常见到函数的概念。但是你了解C语言中的函数吗?

维基百科中对函数的定义:子程序

  • 在计算机科学中,子程序(英语:Subroutine, procedure, function, routine, method, subprogram, callable unit),是一个大型程序中的某部分代码, 由一个或多个语句块组成。它负责完成某项特定任务,而且相较于其他代码,具备相对的独立性。
  • 一般会有输入参数并有返回值,提供对过程的封装和细节的隐藏。这些代码通常被集成为软件库。

8.2 函数的分类

8.2.1 库函数

为什么会有库函数?

  1. 我们知道在我们学习C语言编程的时候,总是在一个代码编写完成之后迫不及待的想知道结果,想把这个结果打印到我们的屏幕上看

    看。这个时候我们会频繁的使用一个功能:将信息按照一定的格式打印到屏幕上(printf)。

  2. 在编程的过程中我们会频繁的做一些字符串的拷贝工作(strcpy)。

  3. 在编程是我们也计算,总是会计算n的k次方这样的运算(pow)。

​ 像上面我们描述的基础功能,它们不是业务性的代码。我们在开发的过程中每个程序员都可能用的到,为了支持可移植性和提高程序

的效率,所以C语言的基础库中提供了一系列类似的库函数,方便程序员进行软件开发。

那怎么学习库函数呢?

这里我们简单的看看:www.cplusplus.com

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

简单的总结,C语言常用的库函数都有:

  • IO函数

  • 字符串操作函数

  • 字符操作函数

  • 内存操作函数

  • 时间/日期函数

  • 数学函数

  • 其他库函数

我们参照文档,学习几个库函数:(教会学生怎么使用文档来学习库函数)。

char * strcpy ( char * destination, const char * source );//destination:目的地;source:源头
//char * 指针,存的是地址

#include <stdio.h>
int main()
{
    char arr1[] = "bit";
    char arr2[20] = "##########"
        			bit\0#####------\0字符串结束标志,\0后面不打印
    //把arr2里的内容拷贝到arr1里面去
    strcpy()
    return 0;
}
void * memset ( void * ptr, int value, size_t num );//memory:内存 set 设置
//复制字符 value(一个无符号字符)到参数 ptr 所指向的字符串的前 num 个字符。

#include <stdio.h>
main()
{
    char arr[] = "hello world";
    memset(arr,'*',5);
    //***** world
}

注:

但是库函数必须知道的一个秘密就是:使用库函数,必须包含 #include 对应的头文件。

这里对照文档来学习上面几个库函数,目的是掌握库函数的使用方法。

8.2.1.1 如何使用库函数

需要全部记住吗?No

需要学会查询工具的使用:

www.cplusplus.com

cppreference

英文很重要。最起码得看懂文献。

8.2.2 自定义函数

如果库函数能干所有的事情,那还要程序员干什么?

所有更加重要的是自定义函数

自定义函数和库函数一样,有函数名,返回值类型和函数参数。

但是不一样的是这些都是我们自己来设计。这给程序员一个很大的发挥空间。

函数的组成:

ret_type fun_name(para1, * )
{
	 statement;//语句项
}
ret_type 返回类型
fun_name 函数名
para1    函数参数

我们举一个例子:

写一个函数可以找出两个整数中的最大值。

#include <stdio.h>

//找出两个整数中的最大值,仅仅只是找出最大值而已,不会改变num1和num2的值,所以传值就可以了,不需要传地址
//get_max函数的设计
int get_max(int x, int y)
{
 	 return (x > y) ? (x) : (y);
}
int main()
{
     int num1 = 10;
     int num2 = 20;
     int max = get_max(num1, num2);//把num 1,num2传给get_max函数
     printf("max = %d\n", max);
     return 0;
}

再举个例子:

写一个函数可以交换两个整形变量的内容。

#include <stdio.h>
//实现成函数,但是不能完成任务,没有交换
//a和b的空间与x和y的空间不一样,x跟y的值交换了,但跟a和b没有关系,得使用相同的地址空间

//当实参传给形参的时候
//形参其实是实参的一份临时拷贝
//对形参的修改是不会改变实参的
void Swap1(int x, int y)
{
     int tmp = 0;
     tmp = x;
     x = y;
     y = tmp;
}
//正确的版本
void Swap2(int *px, int *py)//void 调用就好了,不需要返回值
{
     int tmp = 0;
     tmp = *px;
     *px = *py;
     *py = tmp;
}
int main()
{
     int num1 = 10;
     int num2 = 20;
     Swap1(num1, num2);
     printf("Swap1:num1 = %d num2 = %d\n", num1, num2);//10 20
     //int* pa = &num1;//pa指针变量
     //*pa  解引用操作
     Swap2(&a, &b);
     printf("Swap2::num1 = %d num2 = %d\n", num1, num2);//20 10
     return 0;
}

8.3 函数参数

8.3.1 实际参数(实参)

真实传给函数的参数,叫实参。

实参可以是:常量、变量、表达式、函数等。

无论实参是何种类型的量,在进行函数调用时,它们都必须有确定的值,以便把这些值传送给形参。

8.3.2 形式参数(形参)

形式参数是指函数名后括号中的变量,因为形式参数只有在函数被调用的过程中才实例化(分配内存单元),所以叫形式参数。形式参数当函数调用完成之后就自动销毁了。因此形式参数只在函数中有效。

上面 Swap1 和 Swap2 函数中的参数 x,y,px,py 都是形式参数。在main函数中传给 Swap1 的 num1 ,

num2 和传给 Swap2 函数的 &num1 , &num2 是实际参数

这里我们对函数的实参和形参进行分析:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

实参num1 和 num2 ;形参 x,y 使用的不是同一空间。

代码对应的内存分配如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

这里可以看到 Swap1 函数在调用的时候, x , y 拥有自己的空间,同时拥有了和实参一模一样的内容。

所以我们可以简单的认为:形参实例化之后其实相当于实参的一份临时拷贝

即:当实参传给形参的时候,形参其实是实参的一份临时拷贝,对形参的修改是不会改变实参的

8.4 函数调用

8.4.1 传值调用

函数的形参和实参分别占有不同内存块,对形参的修改不会影响实参。

8.4.2 传址调用
  • 传址调用是把函数外部创建变量的内存地址传递给函数参数的一种调用函数的方式。

  • 这种传参方式可以让函数和函数外边的变量建立起真正的联系,也就是函数内部可以直接操作函数外部的变量

8.4.3 练习
  1. 写一个函数可以判断一个数是不是素数。
  2. 写一个函数判断一年是不是闰年。
  3. 写一个函数,实现一个整形有序数组的二分查找。
  4. 写一个函数,每调用一次这个函数,就会将 num 的值增加1。
int main()
{
 int num = 0;
 //调用函数,使得num每次增加1
 return 0;
}

8.5 函数的嵌套调用和链式访问

函数和函数之间可以根据实际的需求进行组合的,也就是互相调用的。

8.5.1 嵌套调用
#include <stdio.h>
void new_line()
{
 printf("hehe\n");
}
void three_line()
{
    int i = 0;
 for(i=0; i<3; i++)
   {
        new_line();
   }
}
int main()
{
 three_line();
 return 0;
}

函数可以嵌套调用,但是不能嵌套定义。

8.5.2 链式访问

把一个函数的返回值作为另外一个函数的参数。

//例1
#include <stdio.h>
#incldue <sering.h>
int main()
{
    //法1
    len = strlen("abc");//返回值是3
    printf("%d", len);
    //法2
    printf("%d", strlen("abc"));
    return 0;
}
//例2
#include <stdio.h>
#include <string.h>
int main()
{
    char arr[20] = "hello";
 	int ret = strlen(strcat(arr,"bit"));//这里介绍一下strlen函数
	printf("%d\n", ret);
 	return 0;
}
#include <stdio.h>
int main()
{
    printf("%d", printf("%d", printf("%d", 43)));//43(两个字符,先打印再返回一个2)-->432(先打印2,再返回返回一个字符)--	//-4321(最后直接打印1)
    //结果是啥?
    //4321---这里需要注意printf函数的返回值是什么
    //注:printf函数的返回值是 : 如果成功,则返回写入的字符总数,否则返回一个负数。
    return 0;
}

8.6 函数的声明和定义

8.6.1 函数声明
  1. 告诉编译器有一个函数叫什么,参数是什么,返回类型是什么。但是具体是不是存在,函数声明决定不了。
  2. 函数的声明一般出现在函数的使用之前。要满足先声明后使用
  3. 函数的声明一般要放在头文件中的。
8.6.2 函数定义

函数的定义是指函数的具体实现,交待函数的功能实现。

#include <stdio.h>
//函数声明
int Add(int, int);//有没有x和y都可以
int main()
{
    int a = 10;
    int b = 20;
    int sum = 0;
    //函数调用
    sum = Add(a, b);
    printf("%d", sum);
    //函数定义如果写在mian函数的下面,没有函数的声明会有一个警告:“Add”未定义
    //声明的话比较麻烦,直接写在mian函数上面。
    //但声明不是这样用的,存在即合理
    return 0;
}
//函数的定义
int Add(int x, int y)
{
    int z = x + y;
    return z;
}
//正常的使用用法(公式):
//当我有一个加法/减法这样的功能时,我们得单独写一个模块出来,叫add.c源文件和一个add.h头文件
//创建一个add.h头文件,创建一个add.c源文件,
//把函数的定义放在add.c文件,把函数的声明放在add.h文件
//这样,两个文件就构成了加法模块
//未来,你想使用这个函数的时候:
//#include "add.h"
//注:引自己写的函数,头文件使用双引号("")

//好处:可以将一个复杂的功能拆分成几个小功能,分别实现。团队成员可以分别实现几个功能的模块,不用全放在同一个.c文件里面,
//想调用直接#include""即可

test.h的内容

放置函数的声明

#ifndef __TEST_H__ //加不加这句话都没有关系
#define __TEST_H__ //加不加这句话都没有关系
//函数的声明
int Add(int x, int y);
#endif //__TEST_H__ //加不加这句话都没有关系
//这三句话通常可以不写
//未来进行工程化的时候,最好写上去

add.c的内容

放置函数的实现/定义

//函数Add的实现
int Add(int x, int y)
{
 return x + y;
}

test.c的内容

Add函数的调用

#include <stdio.h>
#include "add.h"
int main()
{
    int a = 10;
    int b = 20;
    int sum = Add(a, b);
    printf("%d", sum);
    return 0;
}

这种分文件的书写形式,在三字棋和扫雷的时候,再教学生分模块来写。

8.7 函数递归

8.7.1 什么是递归

程序调用自身的编程技巧称为递归( recursion)。

递归做为一种算法在程序设计语言中广泛应用。 一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解。

递归策略:

只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。

递归的主要思考方式在于:把大事化小

递归需要注意栈溢出(stack overflow)

8.7.2 递归的必要条件
  • 存在限制条件,当满足这个限制条件的时候,递归便不再继续。

  • 每次递归调用之后越来越接近这个限制条件。

8.7.3 练习
  • 练习1(画图讲解):

    接受一个整型值(无符号),按照顺序打印它的每一位。

    例如:

    输入:1234,输出 1 2 3 4

    参考代码:

#include <stdio.h>
void print(int n)
{
     if(n>9)
     {
     	print(n/10);
     }
     printf("%d ", n%10);
     int main()
     {
     	int num = 1234;
     	print(num);
     }
     return 0;
}
  • 练习2(画图讲解)

​ 编写函数不允许创建临时变量,求字符串的长度。

​ 参考代码:

#incude <stdio.h>
int Strlen(const char*str)
{
 if(*str == '\0')
 	return 0;
 else
     return 1+Strlen(str+1);
}
int main()
{
     char *p = "abcdef";
     int len = Strlen(p);
     printf("%d\n", len);
     return 0;
}
8.7.4 递归与迭代
  • 练习3:

    求n的阶乘。(不考虑溢出)

    参考代码:

    int factorial(int n)
    {
     if(n <= 1)
     	return 1;
     else
     	return n * factorial(n-1);
    }
    
  • 练习4:

    求第n个斐波那契数。(不考虑溢出)

    参考代码:

    int fib(int n)
    {
     if (n <= 2)         
     	return 1;
     else
        return fib(n - 1) + fib(n - 2);
    }
    

    但是我们发现有问题

    • 在使用 fib 这个函数的时候如果我们要计算第50个斐波那契数字的时候特别耗费时间。
    • 使用 factorial 函数求10000的阶乘(不考虑结果的正确性),程序会崩溃。

    为什么呢?

    我们发现 fib 函数在调用的过程中很多计算其实在一直重复。

    如果我们把代码修改一下:

    int count = 0;//全局变量
    int fib(int n)
    {
     if(n == 3)
     count++;
     if (n <= 2)         
     return 1;
        else
        return fib(n - 1) + fib(n - 2);
    }
    

    最后我们输出看看count,是一个很大很大的值。

    那我们如何改进呢?

    • 在调试 factorial 函数的时候,如果你的参数比较大,那就会报错: stack overflow(栈溢出)

      这样的信息。

      系统分配给程序的栈空间是有限的,但是如果出现了死循环,或者(死递归),这样有可能导致一

      直开辟栈空间,最终产生栈空间耗尽的情况,这样的现象我们称为栈溢出。

    那如何解决上述的问题:

    1. 将递归改写成非递归。

    2. 使用static对象替代 nonstatic 局部对象。在递归函数设计中,可以使用 static 对象替代 nonstatic 局部对象(即栈对象),这不仅可以减少每次递归调用和返回时产生和释放 nonstatic 对象的开销,而且 static 对象还可以保

      存递归调用的中间状态,并且可为各个调用层所访问。

    比如,下面代码就采用了,非递归的方式来实现:

    //求n的阶乘
    int factorial(int n)
    {
            int result = 1;
            while (n > 1)
           {
                 result *= n ;
                 n -= 1;
           }
            return result;
    }
    //求第n个斐波那契数
    int fib(int n)
    {
         int result;
         int pre_result;
         int next_older_result;
         result = pre_result = 1;
         while (n > 2)
         {
               n -= 1;
               next_older_result = pre_result;
               pre_result = result;
               result = pre_result + next_older_result;
         }
         return result;
    }
    

    提示:

    1. 许多问题是以递归的形式进行解释的,这只是因为它比非递归的形式更为清晰。
    2. 但是这些问题的迭代实现往往比递归实现效率更高,虽然代码的可读性稍微差些。
    3. 当一个问题相当复杂,难以用迭代实现时,此时递归实现的简洁性便可以补偿它所带来的运行时开销。

    函数递归的几个经典题目(自主研究):

    1. 汉诺塔问题

    2. 青蛙跳台阶问题

第9章、数组

9.1 一维数组的创建和初始化

9.1.1 数组的创建

数组是一组相同类型元素的集合。

所有的数组都是由连续的内存位置组成。最低的地址对应第一个元素,最高的地址对应最后一个元素。

数组的创建方式:

type_t   arr_name   [const_n];
//type_t 是指数组的元素类型
//const_n 是一个常量表达式,用来指定数组的大小

数组创建的实例:

//代码1
int arr1[10];
//代码2
int count = 10;
int arr2[count];//数组这时候不可以正常创建
//代码3
char arr3[10];
float arr4[1];
double arr5[20];

**注:**数组创建,在C99标准之前, [ ] 中要给一个常量才可以,不能使用变量。在C99标准支持了变长数

组的概念。

9.1.2 数组的初始化

数组的初始化是指,在创建数组的同时给数组的内容一些合理初始值(初始化)。

看代码:

int arr1[10] = {1,2,3};//不完全初始化,剩下的元素默认初始化为0;
int arr2[] = {1,2,3,4};//不完全初始化,剩下的元素默认初始化为0;
int arr3[5] = {12345}char arr4[3] = {'a',98, 'c'};//a b c烫烫烫
char arr4[3] = {'a',98, 'c'};//a b c
char arr5[] = {'a','b','c'};//a b c 剩下的元素默认初始化为0,但不是\0.虽然\0也显示0
char arr6[] = "abc";//a b c \0  有4个字符

char arr = "abcdef";
printf("%d\n", sizeof(arr));//7
//sizeof 计算arr所占空间的大小
//7个元素-char 7 * 1 = 7
printf("%d\n", strlen(arr));//6
//strlen 求字符串的长度-'\0'之前的字符个数
//[a b c d e f \0]

/*
1. strlen和sizeof没有什么关联
2. strlen 是求字符串长度的 - 只能针对字符串长度 - 库函数 - 使用得引头文件 
3. sizeof 计算变量、数组、类型的大小/所占空间 - 单位是字节 - 操作符	
*/

数组在创建的时候如果想不指定数组的确定的大小就得初始化。数组的元素个数根据初始化的内容来确定。

但是对于下面的代码要区分,内存中如何分配。

char arr1[] = "abc";//a b c \0
char arr2[] = {'a','b','c'};//a b c 单个字符后面没有\0
printf("%d\n", sizeof(arr1));//4
printf("%d\n", sizeof(arr2));//3
printf("%d\n", strlen(arr1));//3
printf("%d\n", atrlen(arr2));//15(随机数)

9.2 一维数组的使用

对于数组的使用我们之前介绍了一个操作符: [] ,下标引用操作符。它其实就数组访问的操作符。

我们来看代码:

#include <stdio.h>
int main()
{
     int arr[10] = {0};//数组的不完全初始化
     //计算数组的元素个数
     int sz = sizeof(arr)/sizeof(arr[0]);
     //对数组内容赋值,数组是使用下标来访问的,下标从0开始。所以:
     int i = 0;//做下标
     for(i=0; i<10; i++)//这里写10,好不好?
     {
        arr[i] = i;
     } 
     //输出数组的内容
     for(i=0; i<10; ++i)
     {
        printf("%d ", arr[i]);
     }
     return 0;
}

总结:

  1. 数组是使用下标来访问的,下标是从0开始。

  2. 数组的大小可以通过计算得到。

int arr[] = "abcdef"//[a] [b] [c] [d] [e] [f] [\0] 
    printf("%c\n", arr[3]);//d
int sz = sizeof(arr)/sizeof(arr[0]);

9.3 一维数组在内存中的存储

接下来我们探讨数组在内存中的存储。

看代码:

#include <stdio.h>
int main()
{
 int arr[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
 int sz = sizeof(arr) / sizeof(arr[0]);
 int i = 0;  
 for(i = 0; i < sz; i++)
 {
 printf("&arr[%d] = %p\n", i, &arr[i]);
 }
 return 0;
}

输出的结果如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

仔细观察输出的结果,我们知道,随着数组下标的增长,元素的地址,也在有规律的递增。int都增加4

由此可以得出结论:数组在内存中是连续存放的。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

9.4 二维数组的创建和初始化

9.4.1 二维数组的创建
//数组创建
int arr[3][4];
char arr[3][5];
double arr[2][4];
9.4.2 二维数组的初始化
//数组初始化
int arr[3][4] = {1,2,3,4};
int arr[3][4] = {{1,2},{4,5}};
int arr[][4] = {{2,3},{4,5}};//二维数组如果有初始化,行可以省略,列不能省略

9.5 二维数组的使用

二维数组的使用也是通过下标的方式。

看代码:

#include <stdio.h>
int main()
{
 int arr[3][4] = {0};
 int i = 0;
 for(i=0; i<3; i++)
 {
 	int j = 0;
 	for(j=0; j<4; j++)
 	{
 		arr[i][j] = i*4+j;
    }
 }
 for(i=0; i<3; i++)
 {
 	int j = 0;
 	for(j=0; j<4; j++)
 	{
 		printf("%d ", arr[i][j]);
 	}
 }
 return 0;
}

9.6 二维数组在内存中的存储

像一维数组一样,这里我们尝试打印二维数组的每个元素。

#include <stdio.h>
int main()
{
 int arr[3][4];
 int i = 0;
 for(i = 0; i < 3; i++)
 {
 	int j = 0;
 	for(j = 0; j < 4; j++)
 	{
 		printf("&arr[%d][%d] = %p\n", i, j,&arr[i][j]);
 	}
 }
 return 0;
}

输出的结果:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

通过结果我们可以分析到,其实二维数组在内存中也是连续存储的。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

9.7 数组越界

数组的下标是有范围限制的。

数组的下规定是从0开始的,如果数组有n个元素,最后一个元素的下标就是n-1。

所以数组的下标如果小于0,或者大于n-1,就是数组越界访问了,超出了数组合法空间的访问。

C语言本身是不做数组下标的越界检查,编译器也不一定报错,但是编译器不报错,并不意味着程序就是正确的,所以程序员写代码时,

最好自己做越界的检查。

#include <stdio.h>
int main()
{
 	int arr[10] = {1,2,3,4,5,6,7,8,9,10};
    int i = 0;
    for(i=0; i<=10; i++)
   {
        printf("%d\n", arr[i]);//当i等于10的时候,越界访问了
   }
 	return 0;
}

二维数组的行和列也可能存在越界。

9.8 数组作为函数参数

往往我们在写代码的时候,会将数组作为参数传个函数,比如:我要实现一个冒泡排序(这里要讲算法思想)函数将一个整形数组排序。

那我们将会这样使用该函数:

9.8.1 冒泡排序

冒泡排序(Bubble Sorting)的基本思想是:通过对待排序序列从后向前(从下标较大的元素开始),依次比较相邻元素

的值,若发现逆序则交换,使值较大的元素逐渐从前移向后部,就像水底下的气泡一样逐渐向上冒。

我们将五个无序:24,69,80,57,13 使用冒泡排序法将其排成一个从小到大的有序数列。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

9.8.2 冒泡排序函数的错误设计
//方法1:
#include <stdio.h>
void bubble_sort(int arr[])
{
     int tmp = 0; //用于辅助交换的变量
     int sz = sizeof(arr) / sizeof(arr[0]);//能否计算元素个数?不可以 算出来的结果为1,我们想要10
     int i = 0;
     //将多轮排序使用外层循环包括起来即可   
     for(i = 0; i < sz - 1; i++)//冒泡排序的轮数
       {
            int j = 0;
            for(j = 0; j < sz - i - 1; j++)
           {
                //如果前面的数>后面的数,就交换
                if(arr[j] > arr[j + 1])
               {
                    int tmp = arr[j];
                    arr[j] = arr[j+1];
                    arr[j+1] = tmp;
               }
           }
       }
}
int main()
{
    //对arr进行排序,排成升序
    int arr[] = {3, 1, 7, 5, 8, 9, 0, 2, 4, 6};
    int sz = sizeof(arr) / sizeof(arr[0])
    int i = 0;
    bubble_sort(arr);//冒泡排序函数是否可以正常排序?不可以
    //arr是数组,我们对数组arr进行传参,实际上传递过去的是数组arr首元素的地址 &arr[0]
    // 形参 实例化 之后其实相当于 实参 的一份 临时拷贝。
    //造成了空间浪费,这是两个不同的空间
    for(i = 0; i < sz; i++)
   {
        printf("%d ", arr[i]);
   }
    return 0;
}

方法1,出问题,那我们找一下问题,调试之后可以看到 bubble_sort 函数内部的 sz ,是1。

难道数组作为函数参数的时候,不是把整个数组的传递过去?

9.8.3 数组名是什么
#include <stdio.h>
int main()
{
    int arr[10] = {1,23,4,5};
 	printf("%p\n", arr);
    printf("%p\n", &arr[0]);
    printf("%d\n", *arr);
    //输出结果
    return 0;
}

结论:

数组名是数组首元素的地址。(有两个例外)

如果数组名是首元素地址,那么:

int arr[10] = {0};
printf("%d\n", sizeof(arr));

为什么输出的结果是:40?

补充:

  1. sizeof(数组名) ---- 数组名代表的是整个数组的大小,sizeof内部单独放一个数组名,单位是字节,即sizeof(数组名),计算的是整个数组的大小。

  2. &数组名 ---- 数组名代表整个数组,&数组名,取出的是整个数组的地址。

#include <stdio.h>

int main()
{
    int arr[] = {1,2,3,4,5,6,7};
    printf("%p\n", arr);//00D3F900
    printf("%p\n", arr+1);//00D3F904
    printf("%p\n", &arr[0]);//00D3F900
    printf("%p\n", &arr[0]+1);//00D3F904
    printf("%p\n", &arr);//00D3F900 代表的是整个数组的地址,但只显示最开始的地址,注意区分
    printf("%p\n", &arr+1);//00D3F91C  直接跳到整个数组后面的地址
    return 0;
}

除此1,2两种情况之外,所有的数组名都表示数组首元素的地址。

9.8.4 冒泡排序函数的正确设计

当数组传参的时候,实际上只是把数组的首元素的地址传递过去了。

所以即使在函数参数部分写成数组的形式: int arr[] 表示的依然是一个指针: int *arr 。

那么,函数内部的 sizeof(arr) 结果是4。

如果 方法1 错了,该怎么设计?

//方法2
void bubble_sort(int arr[], int sz)//参数接收数组元素个数
{
 //代码同上面函数
 //int sz = sizeof(arr) / sizeof(arr[0]);这条语句就不需要了
}
int main()
{
    int arr[] = {3,1,7,5,8,9,0,2,4,6};
    int i = 0;
    int sz = sizeof(arr) / sizeof(arr[0]);//10
    bubble_sort(arr, sz);//可以正常排序
    for(i = 0; i < sz; i++)
   {
        printf("%d ", arr[i]);
   }
    return 0;
}

9.9 数据实例

9.9.1数组的应用实例1:三子棋
9.9.2 数组的应用实例2:扫雷游戏

第10章、操作符

10.1 操作符分类

  1. 算术操作符
  2. 移位操作符
  3. 位操作符
  4. 赋值操作符
  5. 单目操作符
  6. 关系操作符
  7. 逻辑操作符
  8. 条件操作符
  9. 逗号表达式
  10. 下标引用、函数调用和结构成员

10.2 算术操作符

+    -   *   /  %
  1. 除了 % 操作符之外,其他的几个操作符可以作用于整数和浮点数。

  2. 对于 / 操作符如果两个操作数都为整数,执行整数除法。而只要有浮点数执行的就是浮点数除法。

  3. % 操作符的两个操作数必须为整数。返回的是整除之后的余数。

10.3 移位操作符

<< 左移操作符
>> 右移操作符
    
注:移位操作符的操作数只能是整数。
10.3.1 左移操作符

移位规则:

左边抛弃、右边补0

10.3.2 右移操作符

移位规则:

首先右移运算分两种:

  1. 逻辑移位

​ 左边用0填充,右边丢弃

  1. 算术移位

​ 左边用原该值的符号位填充,右边丢弃

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

#include <stdio.h>
int main()
{
    //移(二进制)位操作符
    // << 左移   >> 右移
    int a = 1;
    //整形1占4个字节--32bit位
    //[0000000000 0000000000 0000000000 01]
    //左移一位
    int b = a << 1;
    //0[000000000 0000000000 0000000000 1_]  左边溢出的丢弃,右边补0
    //即:0000000000 0000000000 0000000000 10  十进制为 2 即 1 *2的1次方 = 2
   	//同理:左移两位的话,1 * 2的平方
    return 0;
}

警告⚠ :

对于移位运算符,不要移动负数位,这个是标准未定义的。

例如:

int num = 10;
num>>-1;//error

10.4 位操作符

位操作符有:

& //按位与
| //按位或
^ //按位异或
注:他们的操作数必须是整数。

练习一下:

#include <stdio.h>
int main()
{
    //(二进制)位操作
    //&按位与
    //|按位或
    //^按位异或 :对应的二进制位相同,则为0;对应的二进制位相异,则为1;
	int a = 3;
	int b = 5;
    int c = a & b;
    //011
    //101
    //001 ---- 对应十进制为 1
    return 0//   sizeof  计算的是 变量/类型 所占空间的大小,单位是字节
    printf("d\n", sizeof(a));
    printf("d\n", sizeof(int));
    printf("d\n", sizeof a);
    printf("d\n", sizeof int);//报错
    // 计算的是 变量所占空间的大小,括号可以省略,计算的是 类型所占空间的大小,括号不可以省略
    int arr[10] = {0};
    printf("%d\n", sizeof(arr));//40字节
    int sz = 0;
    //计算数组的元素个数
    //个数 = 数组总大小 / 每个元素的大小
    sz = sizeof(arr) / sizeof(arr[0]);
    printf("%d\n", sz);
}
#include <stdio.h>
int main()
{
     int num1 = 1;
     int num2 = 2;
     num1 & num2;
     num1 | num2;
     num1 ^ num2;
     return 0;
}

一道变态的面试题:

不能创建临时变量(第三个变量),实现两个数的交换。

#include <stdio.h>
int main()
{
     int a = 10;//01010
     int b = 20;//10100
     a = a^b;   //11110
     b = a^b;   //01010
     a = a^b;   //10100
     printf("a = %d b = %d\n", a, b);
     return 0;
}

练习:

编写代码实现:求一个整数存储在内存中的二进制中1的个数。

//参考代码:
//方法1
#include <stdio.h>
int main()
{
     int num  = 10;
     int count = 0;//计数
     while(num)
     {
     	if(num % 2 == 1)
         {
            count++;
         }	
     	num = num / 2;
     }
     printf("二进制中1的个数 = %d\n", count);
     return 0;
}
    //思考这样的实现方式有没有问题?
    //方法2:
#include <stdio.h>
int main()
{
     int num = -1;
     int i = 0;
     int count = 0;//计数
     for(i = 0; i < 32; i++)
     {
     	if(num & (1 << i) )
     	count++; 
     }
     printf("二进制中1的个数 = %d\n",count);
     return 0;
}
//思考还能不能更加优化,这里必须循环32次的。
//方法3:
#include <stdio.h>
int main()
{
 	int num = -1;
 	int i = 0;
 	int count = 0;//计数
     while(num)
     {
        count++;
        num = num & (num - 1);
     }
 	printf("二进制中1的个数 = %d\n", count);
 	return 0;
}
//这种方式是不是很好?达到了优化的效果,但是难以想到。
#include <stdio.h>
int main()
{
    //按(二进制)位取反: ~
    int a = 0;//4个字节,32bit位
    int b = ~a;//b是有符号的整型
    // a  0000000000 0000000000 0000000000 00
    // b  1111111111 1111111111 1111111111 11
    //    1111111111 1111111111 1111111111 10
    //	  1000000000 0000000000 0000000000 01
    //原码、反码、补码
    //只要是整数,内存中储存的都是二进制的的补码
    //负数在内存中存储的时候,存储的是二进制的补码
    //正数:原码、反码、补码相同
    //负数:
    //原码:直接按照正负写出的二进制序列
    //反码:原码的符号位不变,其它位取反得到
    //补码:反码 + 1
    //第一位为符号位,为0是正数,为1是负数
    /*
    int a = -2;
    1000000000 0000000000 0000000000 10 ----原码
    1111111111 1111111111 1111111111 01 ----反码
    1111111111 1111111111 1111111111 10 ----补码
    */
    printf("%d", b)//? //打印的是这个数的原码
    return 0;
}

10.5 赋值操作符

赋值操作符是一个很棒的操作符,他可以让你得到一个你之前不满意的值。也就是你可以给自己重新赋值。

int weight = 120;//体重
weight = 89;//不满意就赋值
double salary = 10000.0;
salary = 20000.0;//使用赋值操作符赋值

赋值操作符可以连续使用,比如:
int a = 10;
int x = 0;
int y = 20;
a = x = y+1;//连续赋值
这样的代码感觉怎么样?
那同样的语义,你看看:
x = y+1;
a = x;
这样的写法是不是更加清晰爽朗而且易于调试。
10.5.1 复合赋值符
+=
-=
*=
/=
%=
>>=
<<=
&=
|=
^=

这些运算符都可以写成复合的效果。

比如:

int x = 10;
x = x+10;
x += 10;//复合赋值
//其他运算符一样的道理。这样写更加简洁。

10.6 单目操作符

10.6.1 单目操作符介绍
!           逻辑反操作
-           负值
+           正值
&           取地址
sizeof      操作数的类型长度(以字节为单位)
~           对一个数的二进制按位取反
--          前置、后置--
++          前置、后置++
*           间接访问操作符(解引用操作符)
(类型)       强制类型转换

演示代码:

#include <stdio.h>
int main()
{
 int a = -10;
 int *p = NULL;
 printf("%d\n", !2);
 printf("%d\n", !0);
 a = -a;
 p = &a;
 printf("%d\n", sizeof(a));
 printf("%d\n", sizeof(int));
 printf("%d\n", sizeof a);//这样写行不行?
 printf("%d\n", sizeof int);//这样写行不行?
 return 0;
}

关于sizeof其实我们之前已经见过了,可以求变量(类型)所占空间的大小。

#include <stdio.h>
int main()
{
    int a = 10;
    int b = a++;//后置++,先使用,再++ 
    printf("a= %d b = %d\n", a, b);//11, 10 
    int c = ++a;//前置++,先++,再使用
    printf("a= %d c = %d\n", a, c);//12 , 12
    //a--, --a 同理
    return 0}
#include <stdio.h>
int main() 
{
    // () 强制类型转换
    int a = 3.14;// 从double 转换到 int 可能丢失数据
    int a = (int)3.14;
    return 0;
}
10.6.2 sizeof和数组
#include <stdio.h>
void test1(int arr[])
{
 printf("%d\n", sizeof(arr));//(2)
}
void test2(char ch[])
{
 printf("%d\n", sizeof(ch));//(4)
}
int main()
{
 int arr[10] = {0};
 char ch[10] = {0};
 printf("%d\n", sizeof(arr));//(1)
 printf("%d\n", sizeof(ch));//(3)
 test1(arr);
 test2(ch);
 return 0;
}
问:
(1)、(2)两个地方分别输出多少?
(3)、(4)两个地方分别输出多少?
//++和--运算符
//前置++和--
#include <stdio.h>
int main()
{
    int a = 10;
    int x = ++a;
     //先对a进行自增,然后对使用a,也就是表达式的值是a自增之后的值。x为11。
    int y = --a;
    //先对a进行自减,然后对使用a,也就是表达式的值是a自减之后的值。y为10;
    return 0;
}
//后置++和--
#include <stdio.h>
int main()
{
    int a = 10;
    int x = a++;
    //先对a先使用,再增加,这样x的值是10;之后a变成11;
    int y = a--;
    //先对a先使用,再自减,这样y的值是11;之后a变成10;
    return 0;
}

10.7 关系操作符

关系操作符

>
>=
<
<=
!=   用于测试“不相等”
==      用于测试“相等”

这些关系运算符比较简单,没什么可讲的,但是我们要注意一些运算符使用时候的陷阱。

警告:

在编程的过程中== 和=不小心写错,导致的错误。

10.8 逻辑操作符

逻辑操作符有哪些:

&&     逻辑与
||     逻辑或

区分逻辑与按位与

区分逻辑或按位或

1&2----->0
1&&2---->1
1|2----->3
1||2---->1

逻辑与和或的特点:

#include <stdio.h>
//逻辑操作符:&& 逻辑与 || 逻辑或 注意区分按位与 & 跟按位或 |
// 0 表示 假,一切的 非0 表示 真
int main()
{
    int a = 3;
    int b = 5;
    int c = a && b;//a跟b都是非0,都为真,有一个为假,则结果就为假
    printf("c = %d", c);//1
    //逻辑或
    int d = a || b;//两边为假,结果才为假
    printf(" d = %d", d);//1
    return 0;
}

360笔试题

#include <stdio.h>
int main()
{
    int i = 0,a = 0,b = 2,c = 3,d = 4;
    i = a++ && ++b && d++;
    //i = a++||++b||d++;
    printf("a = %d\n b = %d\n c = %d\nd = %d\n", a, b, c, d);
    return 0;
}
//程序输出的结果是什么?

10.9 条件操作符

exp1 ? exp2 : exp3

练习:

//条件操作符/三目操作符(因为有三个操作数)
//exp1(表达式) ? exp2 : exp3;//整体是一个表达式
//即 表达式1的结果如果为真,则执行exp2,否则执行exp2
#include <stdio.h>
int main()
//找两个数的最大值
{
    int a = 10;
	int b = 20;
    int max = 0;
    if (a > b) 
    {
    	max = a;
	} else 
    {
        max = b;
	}
    //等价
    max = (a > b ? a : b);//= 为赋值运算符
    return 0;
}
1.
if (a > 5)
        b = 3;
else
        b = -3;
转换成条件表达式,是什么样?
2.使用条件表达式实现找两个数中较大值

10.10 逗号表达式

exp1, exp2, exp3, …expN

逗号表达式,就是用逗号隔开的多个表达式。

逗号表达式,从左向右依次执行。整个表达式的结果是最后一个表达式的结果。

//代码1
int a = 1;
int b = 2;
int c = (a>b, a=b+10, a, b=a+1);//逗号表达式
c是多少?
//代码2
if (a =b + 1, c=a / 2, d > 0)
//代码3
a = get_val();
count_val(a);
while (a > 0)
{
    //业务处理
        a = get_val();
        count_val(a);
}
如果使用逗号表达式,改写:
while (a = get_val(), count_val(a), a>0)
{
         //业务处理
}
exp1, exp2, exp3, …expN
//有逗号隔开的一串表达式就是逗号表达式
    
//下标引用、函数调用和结构成员
//[] () . ->
#incldue <stdio.h>
int Add(int x, int y)//定义函数
{
    int z = 0;
    z = x + y;
    return z;
}
int main()
{
    int arr[10] = {0};
    arr[4];//[]----下标引用操作符
    int sum = Add(a, b);//函数调用()---函数调用操作符
    return 0;
}

10.11 下标引用、函数调用和结构成员

  1. 下标引用操作符

​ 操作数:一个数组名 + 一个索引值

int arr[10];//创建数组
arr[9] = 10;//实用下标引用操作符。
[ ]的两个操作数是arr和9
  1. ( ) 函数调用操作符

​ 接受一个或者多个操作数:第一个操作数是函数名,剩余的操作数就是传递给函数的参数。

#include <stdio.h>
 void test1()
 {
 printf("hehe\n");
 }
 void test2(const char *str)
 {
 printf("%s\n", str);
 }
 int main()
 {
 test1();            //实用()作为函数调用操作符。
 test2("hello bit.");//实用()作为函数调用操作符。
 return 0;
 }
  1. 访问一个结构的成员

. 结构体.成员名

-> 结构体指针->成员名

#include <stdio.h>
struct Stu
{
 	char name[10];
 	int age;
 	char sex[5];
 	double score;
}void set_age1(struct Stu stu)
{
 	stu.age = 18;
}
void set_age2(struct Stu* pStu)
{
 	pStu->age = 18;//结构成员访问
}
int main()
{
 	struct Stu stu;
 	struct Stu* pStu = &stu;//结构成员访问
 
 	stu.age = 20;//结构成员访问
 	set_age1(stu);
 
 	pStu->age = 20;//结构成员访问
 	set_age2(pStu);
 	return 0;
}

10.12 表达式求值

表达式求值的顺序一部分是由操作符的优先级和结合性决定。

同样,有些表达式的操作数在求值的过程中可能需要转换为其他类型。

10.12.1 隐式类型转换

C的整型算术运算总是至少以缺省整型类型的精度来进行的。

为了获得这个精度,表达式中的字符和短整型操作数在使用之前被转换为普通整型,这种转换称为整型

提升

整型提升的意义

表达式的整型运算要在CPU的相应运算器件内执行,CPU内整型运算器(ALU)的操作数的字节长度

一般就是int的字节长度,同时也是CPU的通用寄存器的长度。

因此,即使两个char类型的相加,在CPU执行时实际上也要先转换为CPU内整型操作数的标准长度。

通用CPU(general-purpose CPU)是难以直接实现两个8比特字节直接相加运算(虽然机器指令

中可能有这种字节相加指令)。所以,表达式中各种长度可能小于int长度的整型值,都必须先转

换为int或unsigned int,然后才能送入CPU去执行运算。

//实例1
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

整形提升的例子:

//实例1
int main()
{
 char a = 0xb6;
 short b = 0xb600;
 int c = 0xb6000000;
 if(a==0xb6)
 printf("a");
 if(b==0xb600)
 printf("b");
 if(c==0xb6000000)
 printf("c");
 return 0;
}

实例1中的a,b要进行整形提升,但是c不需要整形提升

a,b整形提升之后,变成了负数,所以表达式 a0xb6 , b0xb600 的结果是假,但是c不发生整形提升,则表

达式 c==0xb6000000 的结果是真.

所程序输出的结果是:

c

//实例2
int main()
{
 char c = 1;
 printf("%u\n", sizeof(c));
 printf("%u\n", sizeof(+c));
 printf("%u\n", sizeof(-c));
 return 0;
}

实例2中的,c只要参与表达式运算,就会发生整形提升,表达式 +c ,就会发生提升,所以 sizeof(+c) 是4个字

节.

表达式 -c 也会发生整形提升,所以 sizeof(-c) 是4个字节,但是 sizeof© ,就是1个字节.

10.12.2 算数运算

如果某个操作符的各个操作数属于不同的类型,那么除非其中一个操作数的转换为另一个操作数的类

型,否则操作就无法进行。下面的层次体系称为寻常算术转换

long double
double
float
unsigned long int
long int
unsigned int
int

如果某个操作数的类型在上面这个列表中排名较低,那么首先要转换为另外一个操作数的类型后执行运算。

警告:

但是算术转换要合理,要不然会有一些潜在的问题。

float f = 3.14;
int num = f;//隐式转换,会有精度丢失
10.12.3 操作符的属性

复杂表达式的求值有三个影响的因素。

  1. 操作符的优先级

  2. 操作符的结合性

  3. 是否控制求值顺序。

两个相邻的操作符先执行哪个?取决于他们的优先级。如果两者的优先级相同,取决于他们的结合性。
操作符优先级:
在这里插入图片描述
一些问题表达式

//表达式的求值部分由操作符的优先级决定。
//表达式1
a*b + c*d + e*f

注释:代码1在计算的时候,由于*比+的优先级高,只能保证,*的计算是比+早,但是优先级并不

能决定第三个*比第一个+早执行。

所以表达式的计算机顺序就可能是:

a*b
c*d
a*b + c*d
e*f
a*b + c*d + e*f
或者:
a*b
c*d
e*f
a*b + c*d
a*b + c*d + e*f
//表达式2
c + --c;

注释:同上,操作符的优先级只能决定自减–的运算在+的运算的前面,但是我们并没有办法得知,+操作符的左操作数的获取在右操作数之前还是之后求值,所以结果是不可预测的,是有歧义的。

//代码3-非法表达式
int main()
{
 int i = 10;
 i = i-- - --i * ( i = -3 ) * i++ + ++i;
 printf("i = %d\n", i);
 return 0;
}

表达式3在不同编译器中测试结果:非法表达式程序的结果。

//代码4
int fun()
{
     static int count = 1;
     return ++count;
}
int main()
{
     int answer;
     answer = fun() - fun() * fun();
     printf( "%d\n", answer);//输出多少?
     return 0;
}

这个代码有没有实际的问题?

有问题!

虽然在大多数的编译器上求得结果都是相同的。

但是上述代码 answer = fun() - fun() * fun(); 中我们只能通过操作符的优先级得知:先算乘法,

再算减法。

函数的调用先后顺序无法通过操作符的优先级确定。

//代码5
#include <stdio.h>
int main()
{
 int i = 1;
 int ret = (++i) + (++i) + (++i);
 printf("%d\n", ret);
 printf("%d\n", i);
 return 0;
}
//尝试在linux 环境gcc编译器,VS2013环境下都执行,看结果。

Linux环境的结果:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

VS2013环境的结果:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

看看同样的代码产生了不同的结果,这是为什么?

简单看一下汇编代码.就可以分析清楚.

这段代码中的第一个 + 在执行的时候,第三个++是否执行,这个是不确定的,因为依靠操作符的优先级

和结合性是无法决定第一个 + 和第三个前置 ++ 的先后顺序。

总结:我们写出的表达式如果不能通过操作符的属性确定唯一的计算路径,那这个表达式就是存在问题的。

第11章、常见关键字

变量名不能与关键字冲突

auto  break   case  char  const   continue  default  do   double else  enum   
extern float  for   goto(ghoto语句)  if   int   long  register(寄存器关键字)    return   short  signed
sizeof   static struct(结构体关键字)  switch  typedef(类型定义/类型重定义) union(联合体/共用体)  unsigned   void  volatile  while
    
#include <stdio.h>
int main()
{
    //auto 
    int a = 10;//局部变量/自动变量---- a进入它所在的大括号(作用域)后,出了它的作用域就不应该在了,
    //所以这个a是自动创建,自动销毁的。前面应该有一个 auto 的,不过局部变量都有 auto,那还不如把它省略掉,所以基本不写
    return 0;
}

//register
register int a = 10;//建议把 a 定义成寄存器变量(仅仅是建议)。具体能不能把a放到寄存器,取决于编译器,
				  //编译器自己去判断能不能,要不要吧a 放到寄存器里面去
/*
计算机 存储数据
寄存器    //访问速度最快
高速缓存(在寄存器跟内存之间)//访问速度低于寄存器
内存---4G/8G/16G //内存的访问速度高于硬盘的,造价比较高,太大攻坚就买不起所以没有500G内存这种说法
硬盘---500G
//从下往上,访问速度从低到高,空间越来越小
存在原因:
CPU----中央处理器,在计算机上进行一系类运算的时候,要从内存中拿数据到CPU,以前CPU跟内存的访问速度相匹配的
随着发展,CPU的访问速度变得越来越快,但内存访问速度跟不上,所以搭配起来整体上的处理速度也没有变快,所以发明了高速缓存,寄存器
CPU相处里数据,先在寄存器去拿,没有再去高速缓存,没有再去内存,没有再去硬盘
*/



//signed
int a = 20;//int 定义的变量是有符号,担通常把signed省略掉
unsigned int num = 0//num被称为无符号数,即使放一个负数上去,在num看来也是一个正数

11.1 关键字 typedef

typedef 顾名思义是类型定义,这里应该理解为类型重命名。

#include <stdio.h>
int main()
{
    //typedef---类型定义---类型重定义
    typedef unsigned int u_int;
    unsigned int num = 20;//如果觉得太长了,可以用typedef
    u_int num2 = 10;
    //num num2类型一模一样
    return 0
}

11.2 关键字static

在C语言中:

static是用来修饰变量和函数的

  1. 修饰局部变量—称为静态局部变量

  2. 修饰全局变量—称为静态全局变量

  3. 修饰函数—称为静态函数

11.2.1 修饰局部变量
//代码1
#include <stdio.h>
void test()//void 便是没有返回值/返回为空
{
    int a = 1;//局部变量(进入作用域创建,生命周期开始)
    a++;//2
    printf("a = %d\n", a);
}//生命周期到了,销毁,空间还给系统
int main()
{
 int i = 0;
    for(i=0; i<5; i++)
   {
        test();
   }
    return 0;
}
/*
结果为:
a = 2
a = 2
a = 2
a = 2
a = 2
*/

//代码2
#include <stdio.h>
void test()
{
    //static修饰局部变量
    static int a = 1;//静态的局部变量
    i++;
    printf("a = %d ", a);
}
int main()
{
    int i = 0;
    for(i = 0; i < 5; i++)
   {
        test();
        i++;
   }
    return 0;
}
/*
结果为:
a = 2
a = 3
a = 4
a = 5
a = 6
*/

对比代码1和代码2的效果理解static修饰局部变量的意义。

结论:

static修饰局部变量改变了变量的生命周期

让静态局部变量出了作用域依然存在,到程序结束,生命周期才结束。

11.2.2 修饰全局变量
//代码1
//add.c
//tatic 修饰全局变量
int g_val = 2018;

//test.c
int main()
{
    //extern - 声明外部符号的
    extern int g_val;
    printf("%d\n", g_val);
    return 0;
}
//代码2
//add.c
//tatic 修饰全局变量
static int g_val = 2018;

//test.c
int main()
{
    extern int g_val;
    printf("%d\n", g_val);
    //报错:无法解析的外部符号g_val
    //static 修饰全局变量,改变了变量的作用域,让静态的全局变量只能在自己所在的源文件内部使用
    //出了源文件就没法再使用了
    return 0;
}

代码1正常,代码2在编译的时候会出现连接性错误。

结论:

一个全局变量被static修饰,改变了变量的作用域,使得这个全局变量只能在本源文件内使用,不能在其他源文件内使用。extern外部声明也不行

11.2.3 修饰函数
//代码1
//add.c
int Add(int x, int y)
{
    int z = x + y;
    return z;
}
//test.c
extern int Add(int, int);
int main()
{
    //要使用add.c的Add函数:
    //需要声明外部函数
    int sum = Add(a, b);
    printf("sum = %d\n", sum);
    return 0;
    }
//代码2
//add.c
static int Add(int x, int y)
{
   int z = x + y
    return z;
}
//test.c
extern int Add(int, int);
int main()
{
    int sum = Add(a, b);
    printf("sum = %d\n", sum);
    //无法解析的外部符号Add,函数main中引用了该符号
    //static 修饰函数改变了函数的链接属性
    return 0;
    }

代码1正常,代码2在编译的时候会出现连接性错误.

结论:

static 修饰函数改变了函数的链接属性,外部链接属性(无static修饰) —> 内部链接属性(有static修饰)

一个函数被static修饰,使得这个函数只能在本源文件内使用,不能在其他源文件内使用。

剩余关键字后续陆续会讲解。

第12章、#define 定义 常量 和 宏

//#define定义标识符常量
#define MAX1 100
//define定义 宏---标识符的基础上加了参数 

//函数宏的定义
#define MAX(X,Y) (X >Y ? X : Y)
//定义的宏MAX2(X,Y)最终会被MAX2(X,Y)
int main()
{
    //int a = 100;
    int n = MAX;
    
    int a = 10;
    int b = 20;
    //函数的方式
    int max = Max(a, b);
    printf("max = %d", max);
    //宏的方式
    int max = MAX(a,b);
    //编译器会转换成:max = (a > b ? a : b);
    printf("max = %d\n", max);
    return 0;
}
//函数的实现
int Max(int x, int y)
{
    if (x > y)
    {
        return x;
    } else
    {
        return y;
    }
}

第13章、指针

13.1 指针是什么

13.1.1 指针变量

指针理解的2个要点:

  1. 指针是内存中一个最小单元的编号,也就是地址

  2. 平时口语中说的指针,通常指的是指针变量,是用来存放内存地址的变量

总结:指针就是地址,口语中说的指针通常指的是指针变量。

那我们就可以这样理解:

变量是创建内存中的(在内存中分配空间的),每个内存单元都有地址,所以变量也是有地址的。

内存:

内存是电脑上特别重要的存储器,计算机中程序的运行都是在内存中进行的 。

所以为了有效的使用内存,就把内存划分成一个个小的内存单元,每个内存单元的大小是1个字节

为了能够有效的访问到内存的每个单元,就给内存单元进行了编号,这些编号被称为该内存单元的地址

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

我们可以通过&(取地址操作符)取出变量的内存真实地址,把地址可以存放到一个变量中,这个变量就是指针变量

#include <stdio.h>
int main()
{
 int a = 10;//在内存中开辟一块空间
 int *p = &a;//这里我们对变量a,取出它的地址,可以使用&操作符。
    //a变量占用4个字节的空间,这里是将a的4个字节的第一个字节的地址存放在p变量中,p就是一个指针变量。
    //类型是int*
 return 0;
}

总结:

指针变量,用来存放地址的变量。(存放在指针中的值都被当成地址处理)。

13.1.2 指针变量的大小

那这里的问题是:

一个小的单元到底是多大?(1个字节)如何编址?

经过仔细的计算和权衡我们发现一个字节给一个对应的地址是比较合适的。

对于32位的机器,假设有32根地址线,那么假设每根地址线在寻址的时候产生高电平(高电压)和低电平(低电压)就是(1或者0);

那么32根地址线产生的地址就会是:

00000000 00000000 00000000 00000000

00000000 00000000 00000000 00000001

...

11111111 11111111 11111111 11111111


如何产生地址:

计算机------>32位----->32根地址线/数据线------>通电后,走正电(1)负电(0)之分----->00000000000000000000000000000000~~111 1 1 111111111111111111111111111----->可以产生2^32次方个编号------>都对应这内存的一个地址----->最初是以bit为单位,感觉不合适,32位的计算机内存空间只有0.5G,用byte就变成了4G了,所以一个内存空间地址大小就是一个字节----->对应一个编号。

int a = 10//为a申请4个字节的空间
    //怎么知道放哪了
    // & 取地址操作符

取出变量地址如下:

#include <stdio.h>
int main()
{
 int a = 10;
 &a;//取出a的地址
    //注:这里a的4个字节,每个字节都有地址,取出的是第一个字节的地址(较小的地址)0058F734
 printf("%p\n", &a);//打印地址,%p是以地址的形式打印
    //&a 也是一个地址,可不可以存起来?也可以
    p = &a;//p是用来存放a的地址的
    //有一种变量是用来存放地址的--指针变量--p的类型怎么写?
    int *p = &a;//p为一个整型指针变量 //p的类型是int*
    printf("%p\n", p);
    //0058F734  结果一样
    
    //把变量存到地址上,未来想通过这个地址p找到a
    *p = 20//*----解引用操作符/间接引用操作符
        //对p进行解引用操作,找到它所指向的那个对象a,通过*p找到a那个值,改成20;
    printf("%d\n", a);//20
    //指针实例
    char ch = 'w';
    char *pc = &ch;
    *pc = 'a';
    printf("%c\n", ch);
 	return 0;
}
#include <stdio.h>
//指针变量的大小取决于地址的大小
//32位平台下地址是32个bit位(即4个字节)
//64位平台下地址是64个bit位(即8个字节)
int main()
{
    printf("%d\n", sizeof(char *));//8
    printf("%d\n", sizeof(short *));//8
    printf("%d\n", sizeof(int *));//8
    printf("%d\n", sizeof(double *));//8//64位机器
    return 0;
    
    //问题:
    //各种指针的大小都是8,我们直接用一个通用的指针不就行了吗,为什么还要区分类型
    //
}

这里就有2的32次方个地址。

每个地址标识一个字节,那我们就可以给 (2^32Byte == 2^32/1024KB == 232/1024/1024MB==232/1024/1024/1024GB == 4GB) 4G的空闲进行编址。

同样的方法,那64位机器,如果给64根地址线,那能编址多大空间,自己计算。

这里我们就明白:

在32位的机器上,地址是32个0或者1组成二进制序列,那地址就得用4个字节的空间来存储,所以

一个指针变量的大小就应该是4个字节。

那如果在64位机器上,如果有64个地址线,那一个指针变量的大小是8个字节,才能存放一个地址。

总结:

指针是用来存放地址的,地址是唯一标示一块地址空间的。

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

13.2 指针和指针类型

这里我们在讨论一下:指针的类型

我们都知道,变量有不同的类型,整形,浮点型等。那指针有没有类型呢?

准确的说:有的。

当有这样的代码:

int num = 10;
p = &num;

要将&num(num的地址)保存到p中,我们知道p就是一个指针变量,那它的类型是怎样的呢?

我们给指针变量相应的类型。

char *pc = NULL;
int *pi = NULL;
short *ps = NULL;
long *pl = NULL;
float *pf = NULL;
double *pd = NULL;

这里可以看到,指针的定义方式是: type + * 。

其实:

char* 类型的指针是为了存放 char 类型变量的地址。

short* 类型的指针是为了存放 short 类型变量的地址。

int* 类型的指针是为了存放 int 类型变量的地址。

所有实际数据类型,不管是整型、浮点型、字符型,还是其他的数据类型,对应指针的值的类型都是一样的,都是一个代表内存地址的长的十六进制数。

那指针类型的意义是什么?

13.2.1 指针 ±整数
#include <stdio.h>
int a = 0x11223344;
int* pa = &a;
char* pc = &a;
printf("%p\n", pa);//0095FB58
printf("%p\n", pa+1);//0095FB5C

printf("%p\n", pc);//0095FB58
printf("%p\n", pc+1);//0095FB59
#include <stdio.h>
//演示实例
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;
}

**总结:**指针的类型决定了指针向前或者向后走一步有多大(距离)。

即:指针类型 决定了:指针走一步走多远(指针的步长)

int*p; p+1 —>4个字节//对整型来说,+1就跳过一个整型(4个字节)

char*p; p+1 —>1个字节//对字符类型来说,+1就跳过一个字符类型(1个字节)

double*p; p+1 -->8个字节

13.2.2 指针的解引用
//演示实例
#include <stdio.h>
int main()
{
 int n = 0x11223344;
 char *pc = (char *)&n;
 int *pi = &n;
 *pc = 0;   //重点在调试的过程中观察内存的变化。
 *pi = 0;   //重点在调试的过程中观察内存的变化。
 return 0;
}

总结:

指针的类型决定了,对指针解引用的时候有多大的权限(能操作几个字节)。

比如: char* 的指针解引用就只能访问一个字节,而 int* 的指针的解引用就能访问四个字节。

​ int*p; *p能够访问4个字节

​ char*p; *p能够访问1个字节

指针类型决定了指针进行解引用操作的时候,能够访问空间的大小。

13.2.3 指针 ± 整数和指针和的解引用练习
#include <stdio.h>
int main()
{
    int arr[10] = {0};//开辟了10个int类型大小的空间,每个空间的元素都是0
    int* p = arr;//数组名,首元素的地址
    //注:若将 int* p = arr;改成 char* p = arr;下面的需求还能不能实现?不能
    //把数组arr里面的元素都变成1
    int i = 0;
    for (i = 0; i < 10; i++)
    {
        *(p + i) = 1;
    }
    return 0;
}

13.3 野指针

概念: 野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)

13.3.1 野指针成因
  1. 指针未初始化
#include <stdio.h>
int main()
{ 
 int a;//局部变量未初始化,默认是随机值   
 int *p;//局部的指针变量未初始化,默认为随机值
 *p = 20;
 return 0;
}
  1. 指针越界访问
#include <stdio.h>
int main()
{
    int arr[10] = {0};
    int *p = arr;
    int i = 0;
    for(i = 0; i < 12; i++)
   {
        //当指针指向的范围超出数组arr的范围时,p就是野指针
        *(p++) = i;
   }
    return 0;
}
  1. 指针指向的空间释放

这里放在动态内存开辟的时候讲解,这里可以简单提示一下。

#include <stdio.h>
int* test()
{
    int a = 10;//变量a有自己的地址空间
    return &a;
}
int main()
{
    int *p = test();//调用完test函数的时候,变量a就被销毁了,地址空间就还给操作系统了,空间释放了,但此时的空间地址与a的相同,*p记住了a的空间地址,但a变销毁了,a对应的空间就被释放了,对应*p所指向的空间也就成了随机的地址空间
    *p = 20//*p已经不能通过这个地址把20放到变量a的空间里面去,因为访问的空间已经不是当前程序的了
    return 0;
}
13.3.2 如何规避野指针
  1. 指针初始化

  2. 小心指针越界

  3. 指针指向空间释放时,将指针置为NULL(指针不用的时候将其置成空指针)

  4. 避免返回局部变量的地址

  5. 指针使用之前检查有效性

    #include <stdio.h>
    int main()
    {
        int *p = NULL;//如果不知道指向谁,那就给它赋一个空指针
        //NULL---用来初始化指针的,给指针赋值
        //....
        int a = 10;
        p = &a;//初始化
        //当指针被赋值成NULL这样的空指针时,意味着它没有指向一个有效的空间,我们不能使用它
        //
        if(p != NULL)
       {
            *p = 20;
       }
        return 0;
    }
    

    注:

    • p是一个指针变量的名字,表示此指针变量指向的内存地址,如果使用%p来输出的话,它将是一个16进制数。
    • *p表示此指针指向的内存地址中存放的内容,一般是一个和指针类型一致的变量或者常量。
    • &是取地址运算符,&p就是取指针p的地址,&p就表示编译器为变量p分配的内存地址,而因为p是一个指针变量,这种特殊的身份注定了它要指向另外一个内存地址,程序员按照程序的需要让它指向一个内存地址,这个它指向的内存地址就用p表示。

13.4 指针运算

  • 指针 ± 整数
  • 指针 - 指针
  • 指针的关系运算
13.4.1 指针±整数
//指针 +- 整数
#include <stdio.h>
int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int sz = sizeof(arr) / sizeof(arr[0]);	
    int *p = arr;
    //int *p = arr[9];
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d ", *p);//1 2 3 4 5 6 7 8 9 10
		p++;
        //printf("%d ", *p);//10 9 8 7 6 5 4 3 2 1
		//p--;
	}
	return 0;
}
#define N_VALUES 5
float values[N_VALUES];
float *vp;
//指针+-整数;指针的关系运算
for (vp = &values[0]; vp < &values[N_VALUES];)
{
     *vp++ = 0;//先后++vp,再解引用
}
13.4.2 指针-指针
#include <stdio.h>
int main()
{
    char ch[5] = {0};
	int arr[10] = {1,2,3,4,5,6,7,8,9,10};
    printf("%d", &arr[9] - &arr[0]);//9
    //指针 - 指针,其实是得到中间元素的个数
    printf("%d", &arr[0] - &arr[9]);//-9
    //如果我想得到元素个数的话,一定是大地址减小地址;小地址减打地址的绝对值是是我们中间元素的个数
    printf("%d", &arr[9] - &ch[0]);//error,从char*到int*的类型不兼容
    //结果不可预知,讨论中间元素个数不知道是按照整型来逃婚还是按照字符型来讨论
    //指针-指针的前提,是指向同一块空间
	return 0;
}
int my_strlen(char *s)
{
       char *p = s;
       while(*p != '\0' )
              p++;
       return p-s;//指针-指针
}
int main()
{
	//strlen()---求字符串长度
	//递归---模拟实现了strlen-计数器的方式1
	//方式2
	char arr[] = "bit";
	int len = my_strlen(arr);
	printf("%d\n", len);
	return 0;
}
13.4.3 指针的关系运算

指针可以用关系运算符进行比较,如 ==、< 和 >。如果 p1 和 p2 指向两个相关的变量,比如同一个数组中的不同元素,则可对 p1 和 p2 进行大小比较。

下面的程序修改了上面的实例,只要变量指针所指向的地址小于或等于数组的最后一个元素的地址 &var[MAX - 1],则把变量指针进行递增:

#include <stdio.h>
 
const int MAX = 3;
 
int main ()
{
   int  var[] = {10, 100, 200};
   int  i, *ptr;
 
   /* 指针中第一个元素的地址 */
   ptr = var;
   i = 0;
   while ( ptr <= &var[MAX - 1] )
   {
 
      printf("存储地址:var[%d] = %p\n", i, ptr );
      printf("存储值:var[%d] = %d\n", i, *ptr );
 
      /* 指向上一个位置 */
      ptr++;
      i++;
   }
   return 0;
}

当上面的代码被编译和执行时,它会产生下列结果:

存储地址:var[0] = 0x7ffeee2368cc
存储值:var[0] = 10
存储地址:var[1] = 0x7ffeee2368d0
存储值:var[1] = 100
存储地址:var[2] = 0x7ffeee2368d4
存储值:var[2] = 200
for(vp = &values[N_VALUES]; vp > &values[0];)
{
    *--vp = 0;
}

代码简化, 这将代码修改如下:

//避免这样的写法
for(vp = &values[N_VALUES-1]; vp >= &values[0];vp--)
{
    *vp = 0;
}

实际在绝大部分的编译器上是可以顺利完成任务的,然而我们还是应该避免这样写,因为标准并不保证它可行。

标准规定:

允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许与指向第一个元素之前的那个内存位置的指针进行比较。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

你可以那p1和p2比较,但不能拿p1和p3比较。

13.5 NULL指针

在变量声明的时候,如果没有确切的地址可以赋值,为指针变量赋一个 NULL 值是一个良好的编程习惯。赋为 NULL 值的指针被称为指针。

NULL 指针是一个定义在标准库中的值为零的常量。请看下面的程序:

#include <stdio.h>
 
int main ()
{
   int  *ptr = NULL;
 
   printf("ptr 的地址是 %p\n", ptr  );//ptr 的地址是 0x0
 
   return 0;
}

在大多数的操作系统上,程序不允许访问地址为 0 的内存,因为该内存是操作系统保留的。然而,内存地址 0 有特别重要的意义,它表明该指针不指向一个可访问的内存位置。但按照惯例,如果指针包含空值(零值),则假定它不指向任何东西。

如需检查一个空指针,您可以使用 if 语句,如下所示:

if(ptr)     /* 如果 p 非空,则完成 */
if(!ptr)    /* 如果 p 为空,则完成 */

13.6 指针和数组

我们看一个例子:

#include <stdio.h>
int main()
{
 int arr[10] = {1,2,3,4,5,6,7,8,9,0};
    printf("arr     = %p\n", arr);//arr--首元素地址
    printf("&arr[0] = %p\n", &arr[0]);
    //1.&arr-&数组名-数组名不是首元素地址-数组名表示整个数组---&数组名 取出的是整个数组的地址
    //2.sizeof(arr) - sizeof(数组名)-数组名表示整个数组- sizeof(数组名计算的是整个数组的大小)
    return 0;
}

运行结果:

可见数组名和数组首元素的地址是一样的。

结论:数组名表示的是数组首元素的地址。(2种情况除外)

那么这样写代码是可行的:

int arr[10] = {1,2,3,4,5,6,7,8,9,0};
int *p = arr;//p存放的是数组首元素的地址

既然可以把数组名当成地址存放到一个指针中,我们使用指针来访问一个就成为可能。

例如:

#include <stdio.h>
int main()
{
int arr[] = {1,2,3,4,5,6,7,8,9,0};
int *p = arr; //指针存放数组首元素的地址
int sz = sizeof(arr) / sizeof(arr[0]);
int i = 0;
for(i = 0; i < sz; i++)
{
	printf("&arr[%d] = %p <====> p+%d = %p\n", i, &arr[i], i, p+i);
    //printf("%p ====== %p\n",p +i, &arr[i]);
}
return 0;
}

运行结果:

所以 p+i 其实计算的是数组 arr 下标为i的地址。

那我们就可以直接通过指针来访问数组。

如下:

int main()
{
 int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
 int *p = arr; //指针存放数组首元素的地址
 int sz = sizeof(arr) / sizeof(arr[0]);
 int i = 0;
 for (i = 0; i<sz; i++)
 {
 printf("%d ", *(p + i));
 }
 return 0;
}

13.7 二级指针

指针变量也是变量,是变量就有地址,那指针变量的地址存放在哪里?

这就是 二级指针 。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

对于二级指针的运算有:

  • *pa 通过对a中的地址进行解引用,这样找到的是 a , *pa 其实访问的就是 a .
int a = 20;
int *pa = &a;
  • **ppa 先通过 *pa 找到 a ,然后对 a 进行解引用操作: *pa ,那找到的是 a .
int* * ppa = 30;
//等价于*pa = 30;
//等价于a = 30;

//a的类型int, pa的类型int*, ppa的类型int**,
//pa是一个指针,所以有一个*,它指向的是int,int的意思是pa指向的对象a的类型是int
//ppa的int**最右边的那个*,告诉你ppa是一个指针,前面的int*扫诉你ppa指向的对象的类型是int*

13.8 指针数组

指针数组是指针还是数组?

答案:是数组。是存放指针的数组。

数组我们已经知道整形数组,字符数组。

int arr1[5];
char arr2[6];

那指针数组是怎样的?

int* arr3[5];//是什么?

int a = 10;
int b = 20;
int c = 30;
int* arr[3] = {&a,&b,&c};//指针数组--存放指针的数组
int i= 0for (i = 0; i < 3; i++)
{
    printf("%d ", *(arr[i]));
}

arr3是一个数组,有五个元素,每个元素是一个整形指针.

第14章、结构体

14.1 结构体类型的声明

14.1.1 结构的基础知识

结构是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量。

14.2.2 结构的声明

结构体是C语言中特别重要的知识点,结构体使得C语言有能力描述复杂类型。

比如描述学生,学生包含: 名字+年龄+性别+学号 这几项信息。

这里只能使用结构体来描述了。

struct tag
{
 	member-list;//成员列表
}variable-list;//成员变量--暂时不考虑

例如描述一本书:

#include <stdio.h>
#include <stdio.h>
//结构体
//如何表示人、书这样的 复杂对象 呢
    //人有名字,身高,年龄,身份号码....
    //书有书名,作者,出版社,定价,书号....
    //此时已经不能用double,char, int 这些类型表示了
    //此时:复杂对象---结构体---我门自己创造出来的一种类型

//创建一个结构体类型
struct Book
{
    //描述书的相关属性
 	char name[20];// 书名 C语言程序设计
    short price;//价格 55
}Book;//分号不能漏
int main()
{
    //利用结构体类型--创建一个该类型的结构体变量
 	struct Book b1 = {"C语言程序设计", 55};//b1的类型是struct Book
    printf("书名:《%s》\n",b1.name);//书名是数组,打印用%s,用一个点(.)来调用想要的成员属性---- .为结构成员访问操作符
    printf("价格:%d元\n",b1.price);//价格是整型,打印用%s,用一个点(.)来调用想要的成员属性
    //怎么改价格
    b1.price = 15;
    //改书名
    b1.name = "C++";//报错
    //原因:name 是一个数组名,本质上是一个地址
    //方法:
    strcpy (b1.name, "C++");//string-copy----字符串拷贝-库函数-得使用头文件:string.h
    printf("改变后的价格:%d元", b1.price);
    printf("改变后的书名:%s", b1.name);
    return 0;
    
    //b1也是一个变量,也有一个地址
    struct Book* = pb = &b1;//是一种指针类型,定义了一个变量pb, 指向了b1;
    //利用pb打印出书名和价格
    printf("%s\n", (*pb).name);
    printf("%d\n", (*pb).price);
    //觉得麻烦?
    //引出:
    /*
    .    结构体变量.成员
    ->   结构体指针->成员
    */
    printf("%s\n", pb -> name);
    printf("%d\n", pb -> price);
}

例如描述一个学生:

//定义一个结构体类型(等价于int,只是一个类型,没有空间)
//struct - 结构体关键字 Stu - 结构体标签 struct Stu - 结构体类型
struct Stu
{
    //成员变量
     char name[20];//名字
     int age;//年龄
     char sex[5];//性别
     char id[20];//学号
}s1,s2,s3;//s1,s2,s3是3个全局的结构体变量 //但尽量少用全局变量
int main()
{
    struct Stu s;//创建结构体变量(此时才占空间,如int a) //局部变量
	return 0;
}
//另一种写法
//typedef:把类型重新起一个名字
typedef struct Stu
{
    //成员变量
     char name[20];//名字
     int age;//年龄
     char sex[5];//性别
     char id[20];//学号
}Stu;//新的名字叫做Stu
//区别:这里的Stu是一个类型,上面的s1,s2,s2是变量
int main()
{
    struct Stu s1;//struct不能省略
    Stu s2;//等价,可以将struct省略
    return 0;
}
14.1.3 结构成员的类型

结构的成员可以是标量(普通变量)、数组、指针,甚至是其他结构体

14.2 结构体的定义和初始化

有了结构体类型,那如何定义变量,其实很简单。

struct Point
{
     int x;
     int y;
}p1; //声明类型的同时定义变量p1
struct Point p2; //定义结构体变量p2
//初始化:定义变量的同时赋初值。
struct Point p3 = {x, y};

struct Stu        //类型声明
{
     char name[15];//名字
     int age;      //年龄
     char sex[5];//性别
     char id[20];//学号
};
struct Stu s = {"zhangsan", 20};//初始化 //得用大括号进行初始化

struct Node
{
     int data;
     struct Point p;
     struct Node* next; 
}n1 = {10, {4,5}, NULL}; //结构体嵌套初始化
struct Node n2 = {20, {5, 6}, NULL};//结构体嵌套初始化
//打印结构体信息
struct Stu s = {"张三"20"男""20180101"};
//.为结构成员访问操作符
printf("name = %s age = %d sex = %s id = %s\n", s.name, s.age, s.sex, s.id);
//->操作符
struct Stu *ps = &s;
printf("name = %s age = %d sex = %s id = %s\n", ps->name, ps->age, ps->sex, ps-
>id);

14.3 结构体成员访问

  • 结构体变量访问成员

​ 结构变量的成员是通过点操作符(.)访问的。点操作符接受两个操作数。

例如:

我们可以看到 s 有成员 name 和 age ;

那我们如何访问s的成员?

struct S s;
strcpy(s.name, "zhangsan");//使用.访问name成员
s.age = 20;//使用.访问age成员
  • 结构体指针访问指向变量的成员

​ 有时候我们得到的不是一个结构体变量,而是指向一个结构体的指针。

​ 那该如何访问成员。

如下:

struct Stu
{
 	char name[20];
 	int age;
};
void print(struct Stu* ps)//结构体指针
{
 	printf("name = %s   age = %d\n", (*ps).name, (*ps).age);
    //使用结构体指针访问指向对象的成员
 	printf("name = %s   age = %d\n", ps->name, ps->age);
}
int main()
{
    struct Stu s = {"zhangsan", 20};
    print(&s);//结构体地址传参
    return 0;
}

14.4 结构体传参

直接上代码:

struct S
{
 int data[1000];
 int num;
};
//结构体传参
void print1(struct S s)
{
 	printf("%d\n", s.num);
}
//结构体地址传参
void print2(struct S* ps)
{
 	printf("%d\n", ps->num);
}
int main()
{
    struct S s = {{1,2,3,4}, 1000};
    //打印结构体数据
 	print1(s);  //传结构体
 	print2(&s); //传地址
    //printt2更好,因为
    //空间上:实参传给实参的时候,形参只是对实参进行地址拷贝,此时你需要拷贝一模一样大小的数据空间,放到了s里,如果这个结构体		//空间大的话,空间浪费严重,
    //时间上:同时把s里的数据全部都拷贝过去放到s里面也是需要时间
    //print2函数传过去的是地址,无论你结构体多大,无非就是4/8个字节,看你操作系统,传过去到形参的时候只需要创建4/8的结构体指针变量,通过这个指针,然后找回去,找到你的数据进行打印即可。
 return 0;
}

上面的 print1 和 print2 函数哪个好些?

答案是:首选print2函数。

原因:

函数传参的时候,参数是需要压栈的。

如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降。

结论:

结构体传参的时候,要传结构体的地址。

#include <stdio.h>
int Add(int x, int y)
{
    int z = 0;
    z = x + y;
    return z;
}
int main()
{
    int a = 10;
    int b = 20;
    int ret = 0;
    ret = Add(a, b);
    return 0;
    //任何一次函数调用都会在内存里面的栈区上申请空间
}

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

如何使用ArcGIS Pro进行坡度分析

坡度分析是地理信息系统中一种常见的空间分析方法&#xff0c;用于计算地表或地形的坡度&#xff0c;这里为大家介绍一下如何使用ArcGIS Pro进行坡度分析&#xff0c;希望能对你有所帮助。 数据来源 教程所使用的数据是从水经微图中下载的DEM数据&#xff0c;除了DEM数据&…

Python爬虫:http和https介绍及请求

HTTP和HTTPS 学习目标&#xff1a; 记忆 http、https的概念和区别记忆 浏览器发送http请求的过程记忆 http请求头的形式记忆 http响应头的形式了解 http响应状态码 1 为什么要复习http和https 在发送请求&#xff0c;获取响应的过程中 就是发送http或https的请求&#xff0c…

自然语言发展历程

一、基础知识 自然语言处理&#xff1a;能够让计算理解人类的语言。 检测计算机是否智能化的方法&#xff1a;图灵测试 自然语言处理相关基础点&#xff1a; 基础点1——词表示问题&#xff1a; 1、词表示&#xff1a;把自然语言中最基本的语言单位——词&#xff0c;将它转…

中国电子学会2021年9月份青少年软件编程Sc ratch图形化等级考试试卷四级真题

【 单选题 】 1.下面哪个选项程序可以交换下图列表中第2项和第3项的位置&#xff1f; A&#xff1a; B&#xff1a; C&#xff1a; D&#xff1a; 2.雷峰塔景区的门票价格政策是&#xff1a;成人40元/人&#xff1b;6周岁&#xff08;含6周岁&#xff09;以下的实行免票&#…

常用MII接口详解

开放式系统互连 (OSI) 模型 七层开放系统互连 (OSI) 模型中&#xff0c;以太网层 位于最底部两层 - 物理层和数据链路层。 从百兆以太网接口开始 首先是百兆以太网规定的两种接口 介质无关接口 (MII) Media Independent Interface 介质相关接口 (MDI) Medium Depen…

manjaro 安装 wps 教程

内核: Linux 6.6.16.2 wps-office版本&#xff1a; 11.10.11719-1 本文仅作为参考使用, 如果以上版本差别较大不建议参考 安装wps主体 yay -S wps-office 安装wps字体 &#xff08;如果下载未成功看下面的方法&#xff09; yay -S ttf-waps-fonts 安装wps中文语言 yay …

如何用YOLOv8实现图像分割

1. 介绍 在之前的文章中,介绍了如何使用 YOLOv8 在不同的编程语言来检测图片中的对象。然而,YOLOv8 还可以把检测到的目标图像分割出来,本篇文章将介绍如何使用YOLOv8做图片分割。 对象检测的结果是所有检测到的对象的边界框。图像分割的结果是所有检测到的对象的蒙版。它是…

一篇文章简单介绍YOLO v1到v8的演变

大家好&#xff0c;YOLO&#xff08;You Only Look Once&#xff09;是一种流行的目标检测库&#xff0c;它的第一个版本在2015年发布。YOLO工作速度很快&#xff0c;提供了良好的结果&#xff0c;而且预训练模型是公开可用的。该模型迅速变得流行&#xff0c;该项目至今仍在积…

ai学习前瞻-python环境搭建

python环境搭建 Python环境搭建1. python的安装环境2. MiniConda安装3. pycharm安装4. Jupyter 工具安装5. conda搭建虚拟环境6. 安装python模块pip安装conda安装 7. 关联虚拟环境运行项目 Python环境搭建 1. python的安装环境 ​ python环境安装有4中方式。 从上图可以了解…

python之数组,链表,栈,队列

1.数组 优点&#xff1a; 索引操作速度快&#xff1a;通过索引可以直接访问元素&#xff0c;因此索引操作的时间复杂度是 $O(1)$&#xff0c;即常数级 缺点&#xff1a; 插入、删除元素慢&#xff1a; 如果需要在中间或开始位置插入或删除元素&#xff0c;可能需要移动大量…

漫漫数学之旅036

文章目录 经典格言数学习题古今评注名人小传 - 爱因斯坦 经典格言 纯数学在其领域内是逻辑思想的诗歌。——阿尔伯特爱因斯坦 “纯数学在其领域内是逻辑思想的诗歌”这句话体现了爱因斯坦对数学的深刻理解和热爱。在这句话中&#xff0c;爱因斯坦将纯数学比作诗歌&#xff0c;…

mmdetection如何计算准确率、召回率、F1值

1、训练 python tools/train.py configs/fcos/fcosrdweed3.py 2、测试 这一步要加–outresult.pkl&#xff0c;才能计算准确率和召回率 python tools/test.py configs/fcos/fcosrddweed3.py work_dirs/fcosrddweed3/epoch_300.pth --outresultfcos.pkl3、计算准确率和召回率…

三维GIS的业务导向

的确&#xff0c;目前三维GIS以做特效居多&#xff0c;酷炫、亮眼&#xff0c;从二维转到三维&#xff0c;第一眼就给人眼前一亮的感觉&#xff0c;就凭这一项&#xff0c;很多客户就会买单&#xff0c;GIS的客户以政府、科研院所、特种行业为主&#xff0c;买过一次单后&#…

riscv简单常用汇编指令xv6

文章目录 前言entry.Smretasm volatileread csrwrite csrriscv常见csr寄存器 ecall, 系统调用指令cpu执行异常处理指令的三种事件 异常处理相关寄存器用户态trapsret指令页表切换操作用户态系统调用过程总结 内核态trap缺页异常 中断与设备驱动Locking调度文件系统操作系统拥有…

Docker完整版(一)

Docker完整版&#xff08;一&#xff09; 一、Docker概述1.1、Docker简介1.2、Docker的用途1.3、容器与虚拟机的区别1.4、Docker系统架构1.5、Docker仓库 二、Docker引擎2.1、Docker引擎架构2.2、Docker引擎分类2.3、Docker引擎的安装2.4、Docker镜像加速器 三、Docker镜像3.1、…

Android 完整SDK项目中添加对应的JNI与底层通信

安卓应用发消息给底层 近日需要写一个安卓app和底层发消息&#xff0c;这就涉及到java如何到c层的一个逻辑&#xff0c;app已经写好&#xff0c;就差发个消息了。至于如何对接底层&#xff0c;得和写底层的人进一步沟通&#xff0c;本文笔者只写从java层通信到cpp&#xff0c;…

视频远程监控平台EasyCVR集成后播放只有一帧画面的原因排查与解决

智慧安防视频监控平台EasyCVR能在复杂的网络环境中&#xff08;专网、局域网、广域网、VPN、公网等&#xff09;将前端海量的设备进行统一集中接入与视频汇聚管理&#xff0c;平台可支持的接入协议包括&#xff1a;国标GB28181、RTSP/Onvif、RTMP&#xff0c;以及厂家的私有协议…

C++ 字符串OJ

目录 1、14. 最长公共前缀 2、 5. 最长回文子串 3、 67. 二进制求和 4、43. 字符串相乘 1、14. 最长公共前缀 思路一&#xff1a;两两字符串进行比较&#xff0c;每次比较过程相同&#xff0c;可以添加一个函数辅助比较&#xff0c;查找最长公共前缀。 class Solution { pu…

Vue.set:Vue中的数据绑定利器

&#x1f90d; 前端开发工程师、技术日更博主、已过CET6 &#x1f368; 阿珊和她的猫_CSDN博客专家、23年度博客之星前端领域TOP1 &#x1f560; 牛客高级专题作者、打造专栏《前端面试必备》 、《2024面试高频手撕题》 &#x1f35a; 蓝桥云课签约作者、上架课程《Vue.js 和 E…

【JAVA/Web】数组转对象

一. 需求 数组转对象 数组结构 List:[{id:1,code:phone,value:10101001},{id:2,code:name,value:admin},{id:3,code:address,value:XXXXXX} ]二. 数组转对象&#xff08;键值对映射关系&#xff09; 对象结构 object:{phone:10101001,name:admin,address:XXXXXX }2.1 Java…