vscode的C语言编译环境配置:https://www.cnblogs.com/czlhxm/p/11794743.html
一、C语言简介
二、数据类型
序号 | 类型与描述 |
---|---|
1 | 基本类型: 它们是算术类型,包括两种类型:整数类型和浮点类型。 |
2 | 枚举类型: 它们也是算术类型,被用来定义在程序中只能赋予其一定的离散整数值的变量。 |
3 | void 类型: 类型说明符 void 表明没有可用的值。 |
4 | 派生类型: 它们包括:指针类型、数组类型、结构类型、共用体类型和函数类型。 |
定义常量:用大写符号标识
#define 常量名 值
1、整数类型
标准证书类型的存储大小和值范围比较
类型 | 存储大小 | 值范围 |
---|---|---|
char | 1字节 | -128~127 或 0~255 |
unsigned char | 1字节 | 0~255 |
signed char | 1字节 | -128~127 |
int | 2或4字节 | -215 ~ 215-1(-32768~32767)或-231 ~231-1 |
unsigned int | 2或4字节 | 0~ 216-1(0~65535) 或 0~232-1 |
short | 2字节 | -32768~32767 |
unsigned short | 2字节 | 0~65535 |
long | 4字节 | -231 ~231-1 |
unsigned long | 4字节 | 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、浮点类型
类型 | 存储大小 | 值范围 | 精度 |
---|---|---|---|
float | 4字节 | 1.2E-38 ~ 3.4E+038 | 6位小数 |
double | 8字节 | 2.225074E-308 ~ 1.797693E+308 | 15位小数 |
long double | 16字节 | 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 |
++ | 自增运算符,整数值增加 1 | A++ 将得到 11 |
– | 自减运算符,整数值减少 1 | A-- 将得到 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 位运算符
位运算符作用于位,并逐位执行操作。&、 | 和 ^ 的真值表如下所示:
p | q | p & q | p | q | p ^ q |
---|---|---|---|---|
0 | 0 | 0 | 0 | 0 |
0 | 1 | 0 | 1 | 1 |
1 | 1 | 1 | 1 | 0 |
1 | 0 | 0 | 1 | 1 |
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. 形式参数
函数的参数,形式参数,被当作该函数内的局部变量,如果与全局变量同名它们会优先使用。下面是一个实例:
初始化局部变量和全局变量
局部变量被定义时,必须自行对其初始化。定义全局变量时,系统会自动对其初始化,如下所示:
数据类型 | 初始化默认值 |
---|---|
int | 0 |
char | ‘\0’ |
float | 0.000000 |
double | 0.0 |
pointer | NULL |
正确地初始化变量是一个良好的编程习惯,否则有时候程序可能会产生意想不到的结果,因为未初始化的变量会导致一些在内存位置中已经可用的垃圾值。
九、数组
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)、*a | 0行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)