C语言基础笔记

vscode的C语言编译环境配置:https://www.cnblogs.com/czlhxm/p/11794743.html

一、C语言简介

二、数据类型

序号类型与描述
1基本类型: 它们是算术类型,包括两种类型:整数类型和浮点类型
2枚举类型: 它们也是算术类型,被用来定义在程序中只能赋予其一定的离散整数值的变量。
3void 类型: 类型说明符 void 表明没有可用的值。
4派生类型: 它们包括:指针类型、数组类型、结构类型、共用体类型和函数类型。

定义常量:用大写符号标识

#define 常量名 值

1、整数类型

标准证书类型的存储大小和值范围比较

类型存储大小值范围
char1字节-128~127 或 0~255
unsigned char1字节0~255
signed char1字节-128~127
int2或4字节-215 ~ 215-1(-32768~32767)或-231 ~231-1
unsigned int2或4字节0~ 216-1(0~65535) 或 0~232-1
short2字节-32768~32767
unsigned short2字节0~65535
long4字节-231 ~231-1
unsigned long4字节0~232-1
#include <stdio.h>
#include <float.h>
void main()
{
    //单行注释
	/*
		这是一段注释内容,多行注释
	*/
	printf("64位windows操作系统下int长度为:%d \n", sizeof(int)); //输出4
	printf("64位windows操作系统下unsigned int长度为:%d \n", sizeof(unsigned int)); //输出4
	printf("64位windows操作系统下long长度为:%d\n", sizeof(long)); //输出4
	printf("64位windows操作系统下unsigned long长度为:%d \n", sizeof(unsigned long)); //输出4
}

2、浮点类型

类型存储大小值范围精度
float4字节1.2E-38 ~ 3.4E+0386位小数
double8字节2.225074E-308 ~ 1.797693E+30815位小数
long double16字节19位小数
#include <stdio.h>
#include <float.h>
void main()
{
	printf("float类型数据的长度为:%d \n", sizeof(float)); // 4
	printf("float类型的最小值为:%E \n", FLT_MIN); //1.175494E-038
	printf("float类型的最大值为:%E \n", FLT_MAX); //3.402823E+038
	printf("float类型的精度值位:%d \n", FLT_DIG); //6位小数精度

	printf("=========================================================\n");
	printf("double类型数据的长度为:%d \n", sizeof(double)); // 8
	printf("double类型的最小值为:%E \n", DBL_MIN); //2.225074E-308
	printf("double类型的最大值为:%E \n", DBL_MAX); //1.797693E+308
    printf("double类型的精度值位:%d \n", DBL_DIG); //15位小数精度
}

3、void类型

void类型表示指定没有可用的值。有以下三种情况

  • 函数返回为空
  • 函数参数为空
  • 指针指向void : 类型为 void * 的指针代表对象的地址,而不是类型。例如 void *malloc(size_t size); 表示返回指向void的指针,可以转换为任何类型

总结

1、常用基本数据类型占用空间(64位机器)
  • char :1字节
  • int:4字节
  • float:4字节
  • double:8字节
2、基本数据类型书写格式
  • 10进制:没有前缀,默认书写格式
  • 8进制:以0开头,如045,021
  • 2进制:以0b开头,0b10110011
  • 16进制:以0x(0X)开头,如0xF1
  • 小数:
    • 单精度常量:2.3f
    • 双精度常量:2.3 ,不带后缀,表示默认为双精度格式
  • 字符型常量:用单引号括起来,只保存一个字符 ‘a’ ‘b’ , 转义字符 ‘\n’ ‘\t’ 等
  • 字符串常量:用双引号括起来,可以保存多个字符 “abs”
3、数据类型转换

1、在C语言中,如果表达式中含有不同类型的常量和变量,在计算时,会将他们自动转换为同一种类型。也可以对数据类型进行强制转换。

2、自动转换规则:

​ 1) 若参与运算的类型不同,则先转换成同一类型,然后进行运算

​ 2)转换按数据长度增加的方向进行,**以保证精度不降低。**如int和long进行运算时,先把int转换为long再进行运算

​ a. 若两种类型的字节数不同,转换为字节数较高的类型

​ b. 若字节数相同,且有一个是无符号类型,一个是有符号类型,则转成无符号类型。

​ 3)所有的浮点运算都是以双精度进行的即使仅含float单精度量运算的表达式,也要先转换成double型,再作运算。

​ 4)char型和short型参与运算时,必须先转换成int型。

​ 5) 在赋值运算中,赋值号两边量的数据类型不同时,赋值号右边量的类型将转换为左边量的类型。如果右边量的数据类型长度左边长时,将丢失一部分数据,这样会降低精度,丢失的部分按四舍五入向前舍入。

  • 浮点数赋值给整型:该浮点数小数被舍去。
  • 整数赋值给浮点型:数值不变,但是被存储到相应的浮点型变量中。

3、强制类型转换形式:(类型说明符)(表达式)

#include<stdio.h>
void main(){
	float f, x = 3.6, y = 5.2;
	int i = 4, a, b;
	a = x + y;
	b = (int)(x + y);
	f = 10 / i;
    //输出a = 8, b = 8, f = 2.000000, x = 3.600000
	printf("a = %d, b = %d, f = %f, x = %f\n", a, b, f, x); 
}

三、变量

类型描述
char通常是一个字节(八位)。这是一个整数类型。
int对机器而言,整数的最自然的大小。
float单精度浮点值。单精度是这样的格式,1位符号,8位指数,23位小数。[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2vAqG3wC-1608271744566)(C:\Users\李柏松\AppData\Roaming\Typora\typora-user-images\image-20201110084327843.png)]
double双精度浮点值。双精度是1位符号,11位指数,52位小数。[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wUdG6nWV-1608271744571)(C:\Users\李柏松\AppData\Roaming\Typora\typora-user-images\image-20201110084343549.png)]
void表示类型的缺失。

此外,C 语言也允许定义各种其他类型的变量,比如枚举、指针、数组、结构、共用体等等

四、字符串和格式化输入输出

4.1、字符串

字符串就是一个或多个字符的序列。

4.1.1 char数组类型和空字符

C语言将字符串存储在char数组中,每个字符占用一个单元

1、scanf
  • scanf会在遇到第一个**空白字符空格、制表符、或者换行符处停止读取**
#include<stdio.h>
#define PRAISE "What a supper marvelous name!"
void main(){
    char name[40];
    printf("what's your name? \n");
    //注意:scanf会在遇到第一个空白字符空格、制表符、或者换行符处停止读取
    scanf("%s", name);
    printf("Hello, %s. %s\n", name, PRAISE); //Hello, libaisong. What a supper marvelous name!

}

在scanf中,格式控制符与printf函数中的使用方式相同,如%d、%o、%x、%c、%s、%f等等。但是在输入时所有的非格式控制符”都要原样输入。所以在本题中输入的时候b=,f=:以及逗号都必须要原样输入****。仅有选项B符合要求。

五、存储类

  • auto:所有局部变量的默认的存储类,只能用于函数内(修饰局部变量)
  • register:存储用于定义存储在寄存器中而不是RAM中的变量。
    • 变量的最大尺寸等于寄存器的大小
    • 不能对应应用一元的 ’&‘ 运算符
  • static存储类:指示编译器在程序的生命周期内保持局部变量的存在,而不需要在每次它进入和离开作用域时进行创建和销毁
    • 使用 static 修饰局部变量可以在函数调用之间保持局部变量的值
    • static 修饰符也可以应用于全局变量。当 static 修饰全局变量时,会使变量的作用域限制在声明它的文件内。
    • 全局声明的一个 static 变量或方法可以被任何函数或方法调用,只要这些方法出现在跟 static 变量或方法同一个文件中。
  • extern存储类
    • extern 存储类用于提供一个全局变量的引用,全局变量对所有的程序文件都是可见的。当使用 extern 时,对于无法初始化的变量,会把变量名指向一个之前定义过的存储位置。

六、运算符

运算符是一种告诉编译器执行特定的数学或逻辑操作的符号。C 语言有以下类型的运算符:

  • 算术运算符
  • 关系运算符
  • 逻辑运算符
  • 位运算符
  • 赋值运算符
  • 杂项运算符

6.1 算术运算符

运算符描述实例
+把两个操作数相加A + B 将得到 30
-从第一个操作数中减去第二个操作数A - B 将得到 -10
*把两个操作数相乘A * B 将得到 200
/分子除以分母B / A 将得到 2
%取模运算符,整除后的余数B % A 将得到 0
++自增运算符,整数值增加 1A++ 将得到 11
自减运算符,整数值减少 1A-- 将得到 9

6.2 关系运算符

下表显示了 C 语言支持的所有关系运算符。假设变量 A 的值为 10,变量 B 的值为 20,则:

运算符描述实例
==检查两个操作数的值是否相等,如果相等则条件为真。(A == B) 为假。
!=检查两个操作数的值是否相等,如果不相等则条件为真。(A != B) 为真。
>检查左操作数的值是否大于右操作数的值,如果是则条件为真。(A > B) 为假。
<检查左操作数的值是否小于右操作数的值,如果是则条件为真。(A < B) 为真。
>=检查左操作数的值是否大于或等于右操作数的值,如果是则条件为真。(A >= B) 为假。
<=检查左操作数的值是否小于或等于右操作数的值,如果是则条件为真。(A <= B) 为真。

6.3 逻辑运算符

下表显示了 C 语言支持的所有关系逻辑运算符。假设变量 A 的值为 1,变量 B 的值为 0,则:

运算符描述实例
&&称为逻辑与运算符。如果两个操作数都非零,则条件为真。(A && B) 为假。
||称为逻辑或运算符。如果两个操作数中有任意一个非零,则条件为真。(A || B) 为真。
!称为逻辑非运算符。用来逆转操作数的逻辑状态。如果条件为真则逻辑非运算符将使其为假。

6.4 位运算符

位运算符作用于位,并逐位执行操作。&、 | 和 ^ 的真值表如下所示:

pqp & qp | qp ^ q
00000
01011
11110
10011

6.5 赋值运算符

下表列出了 C 语言支持的赋值运算符:

运算符描述实例
=简单的赋值运算符,把右边操作数的值赋给左边操作数C = A + B 将把 A + B 的值赋给 C
+=加且赋值运算符,把右边操作数加上左边操作数的结果赋值给左边操作数C += A 相当于 C = C + A
-=减且赋值运算符,把左边操作数减去右边操作数的结果赋值给左边操作数C -= A 相当于 C = C - A
*=乘且赋值运算符,把右边操作数乘以左边操作数的结果赋值给左边操作数C *= A 相当于 C = C * A
/=除且赋值运算符,把左边操作数除以右边操作数的结果赋值给左边操作数C /= A 相当于 C = C / A
%=求模且赋值运算符,求两个操作数的模赋值给左边操作数C %= A 相当于 C = C % A
<<=左移且赋值运算符C <<= 2 等同于 C = C << 2
>>=右移且赋值运算符C >>= 2 等同于 C = C >> 2
&=按位与且赋值运算符C &= 2 等同于 C = C & 2
^=按位异或且赋值运算符C ^= 2 等同于 C = C ^ 2
|=按位或且赋值运算符

6.6 杂项运算符 ↦ sizeof & 三元

运算符描述实例
sizeof()返回变量的大小。sizeof(a) 将返回 4,其中 a 是整数。
&返回变量的地址。&a; 将给出变量的实际地址。
*指向一个变量。*a; 将指向一个变量。
? :条件表达式如果条件为真 ? 则值为 X : 否则值为 Y

6.7 C 中的运算符优先级

类别运算符结合性
后缀() [] -> . ++ - -从左到右
一元+ - ! ~ ++ - - (type)* & sizeof从右到左
乘除* / %从左到右
加减+ -从左到右
移位<< >>从左到右
关系< <= > >=从左到右
相等== !=从左到右
位与 AND&从左到右
位异或 XOR^从左到右
位或 OR|从左到右
逻辑与 AND&&从左到右
逻辑或 OR||从左到右
条件?:从右到左
赋值= += -= *= /= %=>>= <<= &= ^= |=从右到左
逗号,从左到右

七、函数

函数声明告诉编译器函数的名称、返回类型和参数。函数定义提供了函数的实际主体。

在 C 语言中,函数由一个函数头和一个函数主体组成。下面列出一个函数的所有组成部分:

  • **返回类型:**一个函数可以返回一个值。return_type 是函数返回的值的数据类型。有些函数执行所需的操作而不返回值,在这种情况下,return_type 是关键字 void
  • **函数名称:**这是函数的实际名称。函数名和参数列表一起构成了函数签名。
  • **参数:**参数就像是占位符。当函数被调用时,您向参数传递一个值,这个值被称为实际参数。参数列表包括函数参数的类型、顺序、数量。参数是可选的,也就是说,函数可能不包含参数。
  • **函数主体:**函数主体包含一组定义函数执行任务的语句。
函数参数

如果函数要使用参数,则必须声明接受参数值的变量。这些变量称为函数的形式参数

形式参数就像函数内的其他局部变量,在进入函数时被创建,退出函数时被销毁

当调用函数时,有两种向函数传递参数的方式:

调用类型描述
传值调用该方法把参数的实际值复制给函数的形式参数。在这种情况下,修改函数内的形式参数不会影响实际参数。
引用调用通过指针传递方式,形参为指向实参地址的指针,当对形参的指向操作时,就相当于对实参本身进行的操作。

默认情况下,C 使用传值调用来传递参数。

#include<stdio.h>

void swap(int a, int b);
//指针传递
void swap2(int *x, int *y);

//值传递
void swap(int x, int y){
   
    int temp = x;
    x = y;
    y = temp;
    printf("值传递swap函数的值交换:x = %d, y = %d \n", x, y);
}

//指针传递
void swap2(int *x, int *y){
    int temp = *x;
    *x = *y;
    *y = temp;
}

void main(){
    int x = 9, y = 18;
    printf("原始值为:x = %d, y = %d\n", x, y);
    swap(x,y);
    printf("经过值传递,原始值不改变:x = %d, y = %d\n", x,y);
    swap2(&x, &y); //传递地址
    printf("经过指针传递,原始值改变:x = %d, y = %d \n", x, y);
    swap(x, y);
}
内部函数(static修饰)

如果一个函数只能被本文件中其他函数所调用,它称为内部函数。在定义内部函数时,在函数名和函数类型的前面加 static,即

static 类型名 函数名 (形参表)

例如,函数的首行:

static int max(int a,int b)

内部函数又称静态函数。使用内部函数,可以使函数的作用域只局限于所在文件。即使在不同的文件中有同名的内部函数,也互不干扰。提高了程序的可靠性。

外部函数

如果在定义函数时,在函数的首部的最左端加关键字 extern,则此函数是外部函数,可供其它文件调用。

如函数首部可以为

extern int max (int a,int b)

C 语言规定,如果在定义函数时省略 extern,则默认为外部函数。

在需要调用此函数的其他文件中,需要对此函数作声明(不要忘记,即使在本文件中调用一个函数,也要用函数原型来声明)。在对此函数作声明时,要加关键字 extern,表示该函数是在其他文件中定义的外部函数。

内联函数

内联函数是指用inline关键字修饰的函数。在类内定义的函数被默认成内联函数。内联函数从源代码层看,有函数的结构,而在编译后,却不具备函数的性质。

内联扩展是用来消除函数调用****时的时间开销。它通常用于频繁执行的函数,对于小内存空间的函数非常受益。

使用内联函数的时候要注意:

  • 递归函数不能定义为内联函数

  • 内联函数一般适合于不存在while和switch等复杂的结构且只有1~5条语句的小函数上,否则编译系统将该函数视为普通函数。

  • 内联函数只能先定义后使用,否则编译系统也会把它认为是普通函数。

  • 对内联函数不能进行异常的接口声明。

八、变量作用域

  • 在函数或块内部的局部变量
  • 在所有函数外部的全局变量
  • 在形式参数的函数参数定义中
1. 局部变量

在某个函数或块的内部声明的变量称为局部变量。它们只能被该函数或该代码块内部的语句使用。局部变量在函数外部是不可知的。下面是使用局部变量的实例。

2. 全局变量

全局变量是定义在函数外部,通常是在程序的顶部。全局变量在整个程序生命周期内都是有效的,在任意的函数内部能访问全局变量。

全局变量可以被任何函数访问。也就是说,全局变量在声明后整个程序中都是可用的。

3. 形式参数

函数的参数,形式参数,被当作该函数内的局部变量,如果与全局变量同名它们会优先使用。下面是一个实例:

初始化局部变量和全局变量

局部变量被定义时,必须自行对其初始化。定义全局变量时,系统会自动对其初始化,如下所示:

数据类型初始化默认值
int0
char‘\0’
float0.000000
double0.0
pointerNULL

正确地初始化变量是一个良好的编程习惯,否则有时候程序可能会产生意想不到的结果,因为未初始化的变量会导致一些在内存位置中已经可用的垃圾值。

九、数组

1、传递数组给函数

传递一个一维数组作为参数,以下面三种方式来声明函数形式参数,这三种声明方式的结果是一样的,因为每种方式都会告诉编译器将要接收一个整型指针。同样地,可以传递一个多维数组作为形式参数。

  • 方式1:形参是一个指针

    void myFunction(int *param){
        ...
    }
    
  • 方式2:形参是一个已经定义大小的数组

    void myFunction(int param[10]){
        ...
    }
    
  • 方式3:形参是一个未定义大小的数组

    void myFunction(int param[]){
        ...
    }
    

2、从函数返回数组

C 语言不允许返回一个完整的数组作为函数的参数。但是可以通过==指定不带索引的数组名来返回一个指向数组的指针==。

如果想要从函数返回一个一维数组,必须声明一个返回指针的函数,如下

//声明一个返回指针的函数
int * myFunction(){
    ...
}

另外,C 语言不支持在函数外返回局部变量的地址,除非定义局部变量为 static 变量。

现在,让我们来看下面的函数,它会生成 10 个随机数,并使用数组来返回它们,具体如下:

#include<stdio.h>
#include<stdlib.h>
#include<time.h>

//生产随机数的函数
int * getRandom(){
    static int r[10];
    //int i;

    /*  设置种子:拿当前系统时间作为种子,由于时间是变化的,种子变化,可以产生不相同的随机数。
        如果不使用srand,用rand()产生的随机数,在多次运行,结果是一样的。
    */
    srand((unsigned)time(NULL));
    for(int i = 0; i < 10; i++){
        r[i] = rand();
        printf("r[%d] = %d \n", i, r[i]);
    }

    return r;
}

void main(){
    int *p;
    int i;
    p = getRandom();

    for(i = 0; i < 10; i++){
        printf("*(p + %d) = %d \n",i, *(p+i));
    }
}

3、 指向数组的指针

十、枚举

枚举是 C 语言中的一种基本数据类型,它可以让数据更简洁,更易读。

枚举语法定义格式为:

enum 枚举名 {枚举元素1,枚举元素2,……};

注意:第一个枚举成员的默认值为整型的 0后续枚举成员的值在前一个成员上加 1。也可以指定

#include<stdio.h>

enum Day {spring=5,sumer,autumn=19,winter};
void main(){
    
    enum Day day;
    day = winter; //没有指定值的枚举元素,其值为前一元素加 1。autumn 的值为 19,winter 的值为 20
    printf("%d \n", day);
}

枚举变量的定义

1、先定义枚举类型,在定义枚举变量
//定义枚举类型
enum DAY
{
      MON=1, TUE, WED, THU, FRI, SAT, SUN
};
//定义枚举变量
enum DAY day;
2、定义枚举类型的同时定义枚举变量
enum DAY
{
      MON=1, TUE, WED, THU, FRI, SAT, SUN
} day;
3、省略枚举名称,直接定义枚举变量
enum
{
      MON=1, TUE, WED, THU, FRI, SAT, SUN
} day;

在C 语言中,枚举类型是被当做 int 或者 unsigned int 类型来处理的,所以按照 C 语言规范是没有办法遍历枚举类型的。

不过在一些特殊的情况下,枚举类型必须连续是可以实现有条件的遍历

以下实例使用 for 来遍历枚举的元素:

#include <stdio.h>
 
enum DAY
{
      MON=1, TUE, WED, THU, FRI, SAT, SUN
} day;
int main()
{
    // 遍历枚举元素
    for (day = MON; day <= SUN; day++) {
        printf("枚举元素:%d \n", day);
    }
}

枚举类型转换为整型

#include <stdio.h>
#include <stdlib.h>
 
int main()
{
    enum day
    {
        saturday,
        sunday,
        monday,
        tuesday,
        wednesday,
        thursday,
        friday
    } workday;
 
    int a = 1;
    enum day weekend;
    weekend = ( enum day ) a;  //类型转换
    //weekend = a; //错误
    printf("weekend:%d",weekend);
    return 0;
}

十一、指针

每一个变量都有一个内存位置,每一个内存位置都定义了可使用 & 运算符访问的地址,它表示了在内存中的一个地址。

  • “*” :取值操作
  • “&” :取地址操作

& 和 * 优先级相同,执行顺序是自右向左

指针占用内存:

#include<stdio.h>

void main()
{
	/*
		C语言和C++相同,指针所在内存的大小与编译环境有关:
			编译环境为32位(x86),指针占用 4 个字节
			编译环境为63位(x64),指针占用 8 个字节
	*/
	printf("sizeof(int *) = %d\n", sizeof(int*));
	printf("sizeof(float *) = %d\n", sizeof(float*));
	printf("sizeof(double *) = %d\n", sizeof(double*));
	printf("sizeof(char *) = %d\n", sizeof(char*));
}

空指针

  • 空指针用于给指针变量进行初始化
int* p = NULL;
  • 指针变量不能进行访问

    0~255之间的内存编号是系统占用的,因此不可以访问

    *p = 100; //访问空指针,会报错
    

野指针

/*野指针:指针直接指向一个内存地址,则该指针称为野指针
	int* p = (int*) ox1100; //指针直接指向一个内存地址
*/

程序中,不能操作野指针和空指针

1、三个数按从大到小的顺序输出

#include<stdio.h>

//实现两个数交换
void swap(int *p1, int *p2){
    int temp = *p1;
    *p1 = *p2;
    *p2 = temp;
}

//实现三个数的交换
void exchange(int *p1, int *p2, int *p3){
    //三次交换
    if(*p1 < *p2){
        swap(p1, p2);
    }
    if(*p1 < *p3){
        swap(p1,p3);
    }
    if(*p2 < *p3){
        swap(p2, p3);
    }
}

void main(){
    int a, b, c, *p1, *p2, *p3;

    scanf("%d %d %d", &a, &b, &c);

    p1 = &a;
    p2 = &b;
    p3 = &c;

    exchange(p1,p2,p3);
    
    printf("%d > %d > %d \n", a,b,c);
}

2、数组和指针

1、访问数组元素的三种方式
  • 第一种:通过数组下标访问
  • 第二种:通过数组名的指针(数组名是数组第一个元素的地址)、
  • 第三种,将指针指向数组的首地址
#include<stdio.h>

void main(){

    int a[10];

    printf("输入数据:\n");
    for(int i = 0; i < 10; i++){
        scanf("%d", &a[i]);
    }

    printf("第一种:通过数组下标访问\n");
    for(int i = 0; i < 10; i++){
        //第一种:通过数组下标访问
        printf("%d ", a[i]);
    }

    printf("\n===============================\n");

    //第二种:通过指针
    printf("第二种:通过指针\n");
    for(int i = 0; i < 10; i++){
        printf("%d ", *(a+i));
    }

    printf("\n==============================\n");
    //第三种,将指针指向数组的首地址
    int *p;
    printf("第三种,将指针指向数组的首地址\n");
    for(p = a; p < (a + 10); p++){
        printf("%d ", *p);
    }
}
2、数组名作为函数参数
  • C语言中当用变量名作为函数参数传递时的是变量的值
  • 用数组名作为函数参数时,因为数组名代表的是数组首元素地址,因此传递的值是地址,因此要求形参为指针变量。
总结

如果有一个实参数组,想在函数中改变此数组中的元素的值,实参和形参的对应关系有以下4种情况:

1) 形参和实参都用数组名

2) 实参采用数组名,形参用指针变量

void main(){
    int a[10];
    f(a,10); //实参为数组名,数组第一个元素的地址
}

void f(int *a, int n){ //形参用指针变量
    ...
}

3) 实参和形参都采用指针变量

void main(){
    int a[10];
    int *p = a;
    f(p, 10);
}

void f(int *x, int n){
	...
}

4) 实参为指针,形参为数组名

void main(){
    int a[10];
    int *p = a; 
    f(p,10); //实参为指针
}

void f(int a[], int n){ //形参为数组名
    ...
}

3、多维数组和指针

假设有一个4行4列的二维数组,数组首地址为2000。在内存中二维数组是按行顺序存储

表达形式含义地址
a二维数组名,指向一维数组,a[0],即0行的首地址2000
a[0]、*(a+0)、*a0行0列元素的地址2000
a+1、&a[1]1行首地址2016
a[1]、*(a+1)1行0列元素a[1][0]的地址2016
a[1]+2、*(a+1)+2、&a[1][2]a[1][2]元素的地址2024
*(a[1]+2)、*(*(a+1)+2)、a[1][2]a[1][2]元素的值
1、访问多维数组地址和元素
  • 一维数组数组名就是首地址, 所以**二维数组各行的首地址即为 a[0]、a[1]、a[2]…**
  • 访问二维数组元素a[i][j]的地址:&a[i][j]、*(a+i)+j、a[i]+j
  • 访问二维数组元素a[i][j]的值:a[i][j]、*(*(a+i)+j)、*(a[i]+j)
#include<stdio.h>

void main(){
    int a[3][4] = {1,2,3,4,5,6,7,8,9,10,11,12};

    printf("数组所有数据的地址为:\n");
    for(int i = 0; i < 3; i++){
        for(int j = 0; j < 4; j++){
            printf("a[%d][%d]地址为:%ox\n",i,j,&a[i][j]);
        }
    }
    //一维数组数组名就是首地址,所以各行的首地址即为a(a[0]、a[1]、a[2]...)
    printf("数组首地址:%ox\n",a);
    printf("第1行首地址:%ox, %ox\n",a+1, &a[1]);
    printf("第2行首地址:%ox\n",a+2);
    
    //具体某个元素的地址:三种方式:&a[i][j]、*(a+i)+j、a[i]+j
    printf("a[1][1](第1行第1个元素地址):%ox, %ox, %ox\n", &a[1][1], *(a+1)+1, a[1]+1);
    printf("a[2][3](第2行第3个元素地址):%ox, %ox, %ox\n", &a[2][3], *(a+2)+3,a[2]+3);

    //访问具体数据值:三种方式
    printf("通过数组下标访问:a[%d][%d] = %d\n", 2, 3, a[2][3]);
    printf("通过指针访问:a[%d][%d] = %d\n", 2, 3, *(*(a+2)+3));
    printf("通过数组名和行地址访问:a[%d][%d] = %d\n", 2, 3, *(a[2]+3));
}
2、指针变量(本质是一个变量)

把3行4列二维数组a分解成一维数组a[0]、a[1]、a[2]…之后,设p为指向二维数组的指针变量,可定义为:
i n t ( ∗ p ) [ 4 ] 数 组 指 针 int (*p)[4] 数组指针 int(p)[4]
它表示p是一个指针变量,指向包含4个元素的一维数组。若指向第一个一维数组a[0],其值等于a\a[0]\&a[0][0],p+i就等于a\a[i]\&a[i][0]。

*(p+i)+j就是a[i][j]的地址,*(*(p+i)+j)就是a[i][j]的值

二维数组指针变量说明的一般形式为:

类型说明符 (*指针变量名)[长度];
//数组指针本质是指针,指向数组的指针

//指针数组本质是数组,数组里面存放的是一个一个指针:int *p[];  []优先级高于*,所以p是一个数组,数组中存放数据的类型是int*
  • 类型说明符为所指数组的数据类型
  • 长度表示二维数组分解为多个一维数组时,一维数组的长度。也就是**二维数组的列**
#include<stdio.h>
void main(){
    int a[3][4] = {1,2,3,4,5,6,7,8,9,10,11,12};

    //定义指针变量,数组指针
    int (*p)[4];
    p = a;
    
    printf("使用指针变量访问二维数组的地址和元素值:\n");
    for(int i = 0; i < 3; i++){
        printf("第%d行的首地址为:%ox\n",i,p+i);
    }
    printf("===================================\n");
    printf("指针变量访问数组地址:\n");
    printf("a[%d][%d]的地址为:%ox; 值为:a[%d][%d] = %d\n", 2, 3, *(p+2)+3, 2,3,*(*(p+2)+3));
    
    printf("使用指针数组访问数组所有元素:\n");
    for(int i = 0; i < 3; i++){
        for(int j = 0; j < 4; j++){
            printf("a[%d][%d] = %d, ", i, j, *(*(p+i)+j));
        }
        printf("\n");
    }
}

查询给定行列的数据

#include<stdio.h>

void main(){
    int a[3][4] = {1,2,3,4,5,6,7,8,9,10,11,12};
    int (*p)[4];
    p = a;
    int m, n;
    
    printf("请输入要查询的数据的行数(小于3)和列数(小于4)\n");
    
    printf("m = ");
    scanf("%d",&m);
    while (m > 2 || m < 0)
    {
        printf("请输入正确的行数:\n");
        printf("m = ");
        scanf("%d",&m);
    }
    
    printf("n = ");
    scanf("%d",&n);
    while (n > 3 || n < 0)
    {
        printf("请输入正确的列数:\n");
       printf("n = ");
       scanf("%d",&n);
    }
    printf("a[%d][%d] = %d\n", m, n, *(*(p+m)+n));
}

4、字符串和数组

1、字符串的存取方式
  • 在C语言中,字符串用字符数组存储,并且末尾自动添加一个 ‘\0’ 作为结束标志

  • 字符指针指向一个字符串

#include<stdio.h>

void main(){
    //1. 采用字符数组定义字符串
    char str[] = "我真帅!";
    printf("%s\n",str);

    //2. 采用字符指针定义字符串
    char *str2 = "你真帅2!";
    printf("%s\n",str2);
}
2、字符串中字符的存取方法
  • 下标方法
  • 指针方法
#include<stdio.h>

void main(){
    char str[] = "I love xiangshuang!", b[40], *p1, *p2;
    int i;

    
    //使用数组下标实现
    for(i = 0; *(str+i) != '\0'; i++){
        b[i] = str[i];
    }
    b[i] = '\0'; //注意最后要补上 '\0' 表示字符串结束
    printf("%s\n", b);


    //使用指针方式实现
    p1 = str;
    p2 = b;
    for( ; *p1 != '\0'; p1++, p2++){
        *p2 = *p1;
    }
    *p2 = '\0';
    printf("%s\n",b);

}

总结:

虽然用字符数组和字符指针变量都能实现字符串的存储和运算。但他们二者之间是有区别的,主要有以下几点:

1)字符数组由若干个元素组成,每个元素中放一个字符。而字符指针变量(本质是一个指针)中存放的是地址(字符串第一个字符的地址),决不是将字符串放到字符指针变量中。

2)赋值方式。对字符数组只能对各个元素赋值,不能用以下方法对字符数组赋值

char str[20];
str = "I love...";

而对于字符指针变量,可以采用下面方法赋值

char *a;
a = "I love...";

但注意:赋给a的不是字符,而是字符串的第一个字符的地址

3)对字符指针变量赋初值

char *a = "I love...";
等价于
char *a;
a = "I love...";

而对于数组的初始化:

char str[20] = {"I love..."};
不能等价于
char str[20];
str[] = "I love...";

4)如果定义了一个字符数组,在编译时为它分配内存单元。他有确定的地址。而定义一个字符指针变量时,给指针变量分配内存单元,在其中可以放一个字符变量的地址。也就是说,该指针变量可以指向一个字符整型数据,但如果对它赋予一个地址值,则它并未具体指向一个确定的字符数据

//这段是可以的
char str[10];
scanf("%s",str);

//这段代码一般也能运行,但比较危险
char *a;
scanf("%s",a);

5)指针变量的值是可以改变的

另外,若定义了一个指针变量,并使它指向一个字符串。就可以用下标形式应用指针变量所指向的字符串中的字符

5、指向函数的指针

5.1、用函数指针变量调用函数

可以用指针变量指向整型变量、字符串、数组。也可以指向一个函数。一个函数在编译时被分配一个入口地址,这个**函数的入口地址就称为函数的指针**。

函数指针就是指向函数的指针变量,可以用于调用函数、传递参数。

函数指针的声明

typedef int (*fun_ptr)(int,int);//声明一个指向同样参数、返回值的函数指针类型
#include<stdio.h>

int max(int x, int y){
    return x > y ? x : y;
}

void main(){

    //p是函数指针
    int (*p)(int, int) = &max; //&可以省略。p存放函数max的地址

    int a, b, c, d;
    printf("请输入三个数字\n");
    scanf("%d %d %d",&a,&b,&c);

    //调用函数,与max(max(a,b),c)等价
    d = p(p(a,b),c);

    printf("最大值为:%d\n",d);
}

6、const修饰指针

const修饰指针有三种情况:

  • const修饰指针 — 常量指针
int a = 10;
int b = 10;
const int *p = &a; //在指针前面加const修饰

p = &b; //正确,指向可以修改
*p = 20; //错误,const修饰的是指针,所以取*的操作不允许。指针指向的值不可以修改

常量指针特点:指针的指向可以修改,但指针指向的值不可以更改。

  • const修饰常量 — 指针常量
int a = 10;
int b = 10;
int * const p = &a; //指针常量(const修饰p,前面int*表示是一个指针)

p = &b;//const修饰变量,所以p不能被修改。错误,指针的指向不可以修改
*p = 20; //正确

指针常量特点:指针的指向不可以修改,但指针指向的值可以更改。

  • const既修饰指针、又修饰常量
int a = 10;
int b = 10;

const int* const p = &a; //const既修饰指针、又修饰常量

特点:指针的指向和指针指向的值都不可以修改。

十二、结构体

1、定义结构体变量

1)方式1:先声明结构体类型再定义变量名字
void main()
{
    //声明结构体
    struct student
        {
            int number; //4字节
            char name[20]; //20字节
            char sex; //1字节
            int age; //4字节
            float score; //8字节
            char addr[30]; //30字节
        };
    //定义结构体变量
    struct student student1, student2;

    int len = sizeof(student1);
    printf("共有 %d 个字节\n",len); //输出68,结构体本应该是67字节
}
2)方式2:声明结构体类型的同时定义变量
void main()
{
    //声明结构体的同时定义变量
    struct student
        {
            int number; //4字节
            char name[20]; //20字节
            char sex; //1字节
            int age; //4字节
            float score; //8字节
            char addr[30]; //30字节
        }student1, student2;
    int len = sizeof(student1);
    printf("共有 %d 个字节\n",len); //输出68,结构体本应该是67字节
}
3)直接定义结构体类型变量:不出现结构名
struct 
    {
        int number; //4字节
        char name[20]; //20字节
        char sex; //1字节
        int age; //4字节
        float score; //8字节
        char addr[30]; //30字节
    }student1, student2;

2、结构体嵌套

#include<stdio.h>

void main()
{

    struct date
    {
        int year;
        int month;
        int day;
    };
    
    struct student
    {
        int number; //4
        char name[20]; //20
        char sex; //1
        int age; //4
        float score; //8
        char addr[30]; //30
        struct date birthday; //嵌套了一个日期结构体
    };

    struct student student1, student2;

    int len = sizeof(student1);
    printf("共有 %d 个字节\n",len); // 80字节
}

3、结构体变量引用

1)不能将一个结构体变量作为一个整体进行输入和输出
2)正确引用的方式:结构体变量名.成员名
  • “.”是一个成员运算符,在所有的运算符中优先级最高
3)结构体必须使用到成员操作,不能直接对结构体进行操作
#include<stdio.h>

void main()
{

    struct date
    {
        int year;
        int month;
        int day;
    };
    

    struct student
    {
        int number; //4
        char *name; //20
        char sex; //1
        int age; //4
        float score; //8
        char *addr; //30
        struct date birthday;
    };

    struct student student1, student2;

    student1.name = "李柏松";
    student1.addr = "四川省巴中市通江县";
    student1.birthday.year = 1995;
    student1.birthday.month = 7;
    student1.birthday.day = 25;
    student1.age = 26;

    printf("%s, %d, %s, %d-%d-%d\n",student1.name, student1.age,student1.addr,student1.birthday.year,student1.birthday.month,student1.birthday.day);

    int len = sizeof(student1);
    printf("共有 %d 个字节\n",len); 
    /*
    李柏松, 26, 四川省巴中市通江县, 1995-7-25
    共有 36 个字节
    */
}

4、结构体数组

struct 名字 结构体名[数量];

案例:投票统计系统

#include<stdio.h>
#include<string.h>
void main(){

    //定义一个结构体
    struct person
    {
        /* data */
        char *name;
        int count;
    };

    //结构体数组
    struct person man[4];

    man[0].name = "李柏松";
    man[1].name = "向爽";
    man[2].name = "罗纳尔多";
    man[3].name = "科比";

    for(int i = 0; i < 4; i++){
        man[i].count = 0; //初始化票数为0
    }

    printf("\n候选人有:%s, %s, %s, %s\n\n", man[0].name, man[1].name, man[2].name, man[3].name);

    for(int i = 0; i < 10; i++){
        char temp[20];
        printf("第 %d 位投票,请写下支持的候选人名字:", i+1);
        scanf("%s", &temp);
        for(int i = 0; i < 4; i++){
            //strcmp():字符串比较函数。相等返回0
            if(strcmp(temp,man[i].name) == 0){
                man[i].count++;
                break;
            }
        }
        //printf("\n");
    }

    printf("\n得票数统计如下:\n");
    for(int i = 0; i < 4; i++){
        printf("%s 得票数为:%d\n", man[i].name, man[i].count);
    }

    int max = 0;
    int index = 0;
    for(int i = 0; i < 4; i++){
        if(max < man[i].count){
            max = man[i].count;
            index = i;
            //printf("%d\n",man[i].count);
        }
    }
    printf("\n本次投票的胜利者为:%s\n", man[index].name);
}

5、指向结构体类型数据的指针

结构体变量的指针就是该结构体变量所占数据的内存段的起始地址

结构体指针变量说明的一般形式:

struct person
    {
        /* data */
        char *name;
        int count;
    }student;	

//声明一个结构体指针变量
struct person *pointer_person;
pointer_person = &student; //正确
//pointer_person = &person; //错误。person只是一个结构名
1、结构体指针引用变量的两种方式
  • stuptr->num
  • (*stuptr).num
#include<stdio.h>

void main(){
    //定义一个结构体
    struct student
    {
        int num;
        char *name;
        int age;
        char *addr;
    }boy1 = {101, "李柏松", 25, "四川省巴中市通江县"};
    
    struct student *stuptr;
    stuptr = &boy1;

    printf("%d, %s, %d, %s \n", stuptr->num, stuptr->name, stuptr->age, (*stuptr).addr);
}
2、结构体指针变量做函数参数
1)使用结构体变量的成员作为参数
2)使用结构体变量作为实参
#include<stdio.h>
#include<string.h>

struct Student
{
    int num;
    //char *name;
    char name[20];
    float score[3];
};

void print(struct Student stu);

void main(){
    struct Student stu;

    stu.num = 108;
    strcpy(stu.name,"李柏松"); //如果结构定义为数组 char name[20],则使用这个语句赋值
    //stu.name = "libaisong"; //结构体中定义为指针,使用这个语句赋值
    stu.score[0] = 85.5;
    stu.score[1] = 59;
    stu.score[2] = 98;
   
    print(stu);
}

void print(struct Student stu){
    printf("学号:%d, 姓名:%s, 成绩:语文 %f; 数学 %f; 英语 %f \n", stu.num, stu.name, stu.score[0], stu.score[1], stu.score[2]);
}
3)用指向结构体变量(或数组)的指针作为实参,将结构体变量(或数组)的地址传递给形参
#include<stdio.h>
#include<string.h>

struct Student
{
    int num;
    //char *name;
    char name[20];
    float score[3];
};

void print(struct Student *stu);

void main(){

    struct Student stu;

    //结构体指针
    struct Student *stuptr;
    stuptr = &stu;

    stu.num = 108;
    strcpy(stu.name,"李柏松"); //如果结构定义为数组 char name[20],则使用这个语句赋值
    //stu.name = "libaisong"; //结构体中定义为指针,使用这个语句赋值
    stu.score[0] = 85.5;
    stu.score[1] = 59;
    stu.score[2] = 98;
    
    //实参为结构体指针
    print(stuptr);

}

void print(struct Student *stuptr){

    printf("学号:%d, 姓名:%s, 成绩:语文 %f; 数学 %f; 英语 %f \n", stuptr->num, stuptr->name,stuptr->score[0],stuptr->score[1],stuptr->score[2]);
}

6、结构体作为函数参数

1、值传递

结构体值传递:复制操作,将调用函数中的数据完全的复制一份给形参,所以效率较低

2、地址(指针)传递

函数中的形参改为指针,可以减少内存空间,而且不会复制出新的副本

const:在形参的结构体指针前面加 const 关键字 const struct student* stu ,可以保证子函数中的结构体参数不被修改

#include<iostream>
using namespace std;

struct student
{
	int no;
	string name;
	float score;
};

//结构体值传递:复制操作,将调用函数中的数据完全的复制一份给形参,所以效率较低
void printStruct1(struct student stu) {
	
	//值传递子函数中修改,不会对原始数据进行修改
	stu.score = 59;
	cout << "值传递子函数中 学号:" << stu.no << ", 姓名:" << stu.name << ", 分数:" << stu.score << endl;
}

//函数中的形参改为指针,可以减少内存空间,而且不会复制出新的副本
//在形参的结构体指针前面加 const 关键字 const struct student* stu ,可以保证子函数中的结构体参数不被修改
void printStruct2(const struct student* stu) {
	//地址传递时,修改数据会改变原始数据的值
	//stu->score = 59; //报错,不能被修改
	cout << "地址传递子函数中 学号:" << stu->no << ", 姓名:" << stu->name << ", 分数:" << stu->score << endl;
}

int main()
{
	struct student s1;
	s1.name = "张三"; s1.no = 110; s1.score = 99;

	//printStruct1(s1);
	printStruct2(&s1);
	cout << "主函数中 学号:" << s1.no << ", 姓名:" << s1.name << ", 分数:" << s1.score << endl;
	
	system("pause");
	return 0;
}

6、动态存储分配

1、常用的内存管理函数(三个)

① malloc、calloc、realloc:分配内存空间函数

  • malloc函数
void *malloc(unsigned int size);

作用:在内存的动态存储区中分配一个长度为size的连续空间

返回值:是一个指向分配域起始地址的**指针(类型为void),可以转换为任意类型**

​ 如果此函数未能成功执行(例如内存空间不足),则返回一个空指针(NULL)

  • calloc函数
void *calloc(unsigned int n,unsigned int size);

作用:在内存的动态存储区中分配n个长度为size的连续空间

返回值:指向分配域起始地址的**指针(类型为void)**

​ 如果分配不成功,返回一个空指针(NULL)

​ calloc可以为一维数组开辟动态存储空间,n为数组元素个数,每个元素长度为size

  • realloc函数

    void *realloc(void *ptr, size_t new_size);
    

    作用:修改一个原先已经分配内存块的大小,对内存扩大或缩小。扩大内存:原先的内容保留,将新增加的内存添加到原先内存块的后面。缩小内存:该内存尾部的部分内存被拿掉

②free:释放内存函数

void free(void *p);

作用:释放由p指向的内存区域,是这部分内存区域能被其他变量使用。

​ p是最近一次调用malloc或calloc函数时返回的值。无返回值

7、链表

实现普通链表案例

#include<stdio.h>

struct student
{
    int num;
    char *name;
    float score;
	//结构体实现链表
    struct student *next;
};

void main(){

    struct student a, b, c, *head;
	//链表赋值
    a.num = 101; a.name = "李柏松"; a.score = 99;
    b.num = 102; b.name = "向爽"; b.score = 100;
    c.num = 104; c.name = "张三"; c.score = 59;

    head = &a;
    a.next = &b;
    b.next = &c;
    c.next = NULL; //注意不要忘记最后一个元素的指针赋值为NULL

    while(head){ //等价于while(head != null)
        printf("%d  %s  %5.1f\n", head->num, head->name, head->score);
        head = head->next;
    }
}

建立动态链表

建立动态链表是指在程序执行过程中从无到有地建立起一个链表,即一个一个地开辟结点和输入各结点数据,并建立起前后相连的关系。

十三、内存分区

1、C语言运行之前

1、预处理:宏定义展开、头文件展开、条件编译,不会检查语法

2、编译:检查语法,将预处理文件编译生成汇编文件

3、汇编:将汇编文件生成目标文件(二进制文件)

4、链接:将目标文件链接为可执行程序

程序编译之后,生成了exe可执行程序,未执行该程序之前分为两个区域:

  • **代码区:**存放CPU执行的机器指令、共享的、只读的

  • 全局区:全局变量、静态变量;常量区(包括字符串常量和其他常量)

总结:

  • C/C++在程序运行之前分为全局区和代码区
  • 代码区的特点是共享和只读
  • 全局区中存放**全局变量、静态变量、常量**
  • 常量区中存放const修饰的全局变量和字符串常量

2、运行之后

2.1、栈区
  • 由编译器自动分配释放、存放函数的参数值、局部变量
  • 注意:不要返回局部变量的地址。因为栈区开辟的数据由编译器自动释放
int fun(int b){ //形参数据也放在栈区
    int a = 10;
    return &a; //返回局部变量的地址
}

void main()
{
    int b = 4;
    int *p = fun(b);
    cout<<*p<<endl; //打印10,正确。这是因为编译器做了保留
    cout<<*p<<endl; //打印输出错误,第二次这个数据就被销毁了。
}
2.2、堆区
  • 由程序员分配释放,若程序员不释放,程序结束时由操作系统回收
  • 手动释放利用操作符delete
  • 在C++中,主要利用new在堆区开辟
**类型***作用域***生命周期***存储位置*
auto变量一对{}内当前函数栈区
static局部变量一对{}内整个程序运行期初始化在data段,未初始化在BSS段
extern变量整个程序整个程序运行期初始化在data段,未初始化在BSS段
static全局变量当前文件整个程序运行期初始化在data段,未初始化在BSS段
extern函数整个程序整个程序运行期代码区
static函数当前文件整个程序运行期代码区
register变量一对{}内当前函数运行时存储在CPU寄存器
字符串常量当前文件整个程序运行期data段

3.易混淆概念

代码区:存放程序编译后的二进制代码,不可寻址区

数据区包括堆,栈,全局/静态存储区

全局/静态存储区包括:全局区(extent),静态区(static),常量区(const)

常量区包括:字符串常量、常变量(const)

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值