一、C语言基础

1.第一个C程序

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
int main(){
printf("Hello World!/n");
return 0;
}
//std - 标准
//i - 输入
//o - 输出
//int 为整型
//main() - 主函数 其中()里面写参数
//printf() - 为库函数
//return 0; - 主函数返回值为0
//注意void main(){} 为古老写法,不可用

2.数据类型

类型 存储大小
char 1字节
int 4字节
short 2字节
long 4字节
long long 8字节
float 4字节
double 8字节

注:1byte = 8bit

3.变量、常量

3.1变量

  1. 分为全局变量局部变量
  2. 局部变量全局变量同名时,局部变量优先使用
  3. 局部变量的作用域是变量所在的局部范围生命周期是进入作用域生命周期开始,出作用域生命周期结束
  4. 全局变量的作用域是整个工程;生命周期是整个程序的生命周期
1
2
3
4
5
6
7
8
9
10
#include <stdio.h>

int global = 2025;//全局变量

int main(){
int local = 2024;//局部变量
int global = 2020;//局部变量
printf("%d", global);//输出2020
return 0;
}

3.2常量

常量分类

  • 字面常量
  • const修饰的常变量:1.保护函数参数,2.不能同指针修改数据,3.指针不能修改数据
  • #define定义的标识符常量
  • 枚举常量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>

#define MONTH 12//标识符常量

enum Sex{MALE,FEMALE,SECRET};//{}中的是枚举常量

void f(const int* p){//const保证函数不被修改参数

}

int main(){
3.14;//字面常量
2025;//字面常量

const int con = 2025;//con是const修饰的常变量
con = 2024;//因const修饰,不能直接修改
const int* p1;//不能通过指针修改数据,指针&可以改.const作用的是*p1
int* const p2;//指针指向的地址不能改,*p2指向数据可以改.const作用的是p2
const int* const p3;//都不能改
int* const p = &x;//指针不能指向其他地址
return 0;
}

4.字符串

字符串的结束标识是 \0 的转义字符,在计算字符串长度的时候 \0 是结束标识,不算做字符串内容。

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
int main(){
char arr1[] = "hello";
char arr2[] = {'h', 'e', 'l', 'l', 'o'};
char arr3[] = {'h', 'e', 'l', 'l', 'o', '\0'};
printf("%s\n", arr1);//hello
printf("%s\n", arr2);//hello烫烫烫烫烫蘦ello 缺少'\0'结束,直至\0结束
printf("%s\n", arr3);//hello
return 0;
}

5.转义字符

转义字符 释义
? 在书写连续多个问号时使用,防止他们被解析成三字母词
\‘ 用于表示字符常量’
\“ 用于表示一个字符串内部的双引号
\\ 用于表示一个反斜杆,防止它被解释为一个转义序列符
\a 警告字符,蜂鸣
\b 退格符
\f 进纸符
\n 换行符
\r 回车
\t 水平制表符
\v 垂直制表符
\ddd ddd表示1~3个八进制的数字。如:\120 X
\xdd dd表示2个十六进制数字。如:\x30 0

6.注释

C语言注释风格 /* */ ,缺陷:不能嵌套注释

C++风格的注释 //xxxxx ,可以注释一行也可以注释多行。

7.选择语句

7.1 if语句

1
2
3
4
5
6
if(表达式){
printf("条件为真");
}
else{
printf("条件为假");
}

else的匹配时和它最近的if匹配

1
2
3
4
5
6
7
8
9
if (){

}
if(){

}
else{//该else看缩进是和最上面的if对齐,在c语言中应和最近的if匹配

}
1
2
3
4
int num = 1;
if(5 == num){//5写在前面,防止写成num = 5时出错
printf("hello world!\n");
}

7.2 switch语句

1
2
3
4
switch(整型表达式){
case 整型常量表达式:
语句;
}

break的使用表示结束,否则会一直往下执行。

实际效果是把语句列表划分为不同的分支部分。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <stdio.h>
int main(){
int day = 0;
switch(dat){
case 1:
printf("星期一\n");
case 2:
printf("星期二\n");
break;//注意:在星期一哪里没有加break,那么就会输出星期二,在berak这里停止
case 3:
printf("星期三\n");
break;
case 4:
printf("星期四\n");
break;
case 5:
printf("星期五\n");
break;
case 6:
printf("星期六\n");
break;
case 7:
printf("星期天\n");
break;
}
return 0;
}

在最后一个case语句的后面加上一条break语句。

default子句,当所有case语句都不符合时,程序不会终止,也不会报错,可在任何一个case标签写一个default:标签,这个default子句后面的语句就会执行,每个switch语句中只能出现一条default子句。

在每个switch语句中都放一条default子句是个好习惯,甚至可以在后边再加上一个break

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <stdio.h>
int main(){
int n = 1;
int m = 2;
switch(n){
case 1:
m++;//m=3
case 2:
n++;//n=2
case 3:
switch(n){
case 1:
n++;
case 2:
m++;//m=4
n++;//n=3
break;
}
case 4:
m++;//m=5
break;
default:
break;
}
printf("m = %d, n = %d\n", m, n);//m=5,n=3
return 0;
}

8.循环语句

  • while语句
  • for 语句
  • do …while()语句

8.1 while循环

1
2
3
4
//while 语句结构
while(表达式){
循环语句;
}

break的使用

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
int main(){
int i = 1;
while(i <= 10){
if(i == 5)//如果i=5时,就跳出while循环
break;//结束循环
printf("%d ",i);
i = i+1;
}
return 0;
}
//输出结果:1 2 3 4

break在while循环中的作用:

只要在循环中遇到break,就停止后期的所有循环,直接终止循环

所以:while中的break是用于永久终止循环的

continue介绍

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
int main(){
int i = 1;
while(i <= 10){
if(i == 5)//如果i=5时,就跳转到while判断,不会向下继续执行,陷入死循环
continue;
printf("%d ",i);
i = i+1;
}
return 0;
}
//输出结果:1 2 3 4 后陷入死循环
1
2
3
4
5
6
7
8
9
10
11
12
13
//针对上述代码改进
#include <stdio.h>
int main(){
int i = 1;
while(i <= 10){
i = i+1;
if(i == 5)
continue;
printf("%d ", i);
}
return 0;
}
//输出结果:2 3 4 6 7 8 9 10 11

continue在while循环中的作用:

continue是用于终止本次循环的,也就是本次循环中continue后边的代码不会再执行,直接跳转到while语句的判断部分。进行下一次循环的入口判断。

8.2 for循环

1
2
3
4
5
for(表达式1;表达式2;表达式3)
循环语句;
//表达式1:为初始化部分,用于初始循环变量的
//表达式2:为条件判断部分,用于判断循环时终止
//表示式3:为调整部分,用于循环条件的调整
1
2
3
4
5
6
7
8
9
#include <stdio.h>
int main(){
int i = 0;
for(i = 1; i<= 10; i++){
printf("%d ", i);
}
return 0;
}
//输出结果:1 2 3 4 5 6 7 8 9 10

breakfor的使用

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
int main(){
int i = 0;
for(i = 1; i <= 10; i++){
if(i == 5)
break;//i=5时结束循环
printf("%d ", i);
}
return 0;
}
//输出结果:1 2 3 4

continuefor中的使用

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
int main(){
int i = 0;
for(i = 1; i <=10; i++){
if(i == 5)
continue;//i=5时,跳转到for中执行i++,再判断是否<=10
printf("%d ", i);
}
return 0;
}
//输出结果:1 2 3 4 6 7 8 9 10

for循环一些特殊使用方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <stdio.h>
int main(){
//一、for循环中的初始部分,判断部分,调正部分时可以省略的
for(;;){
printf("hehe\n");
}
int i = 0;
int j = 0;
for(; i < 10; i++){
for(; j < 10; j++){
printf("hehe\n");
}
}

//二、for循环可以嵌套使用
int i = 0;
int j = 0;
for(i = 0; i < 10; i++){
for(j = 0; j < 10; j++){
printf("hehe\n");
}
}

//三、使用多余一个变量控制循环
int x,y;
for(x = 0, y = 0; x < 2 && y < 5; ++x, y++){
printf("hehe\n");
}

return 0;
}

问题:

1
2
3
4
5
6
7
8
9
10
//请问循环要循环多少次
#inlcude <stdio.h>
int main(){
int i = 0;
int k = 0;
for(i = 0, k = 0; k = 0; i++, k++)
k++;
return 0;
}
//结果循环0次,当进入for循环时,由于判断阶段 k不是等于,而是赋值为0,所以为假,结束循环

8.3 do…while()循环

1
2
3
do
循环语句;
while(表达式);

do…while()循环至少执行一次,使用场景有限

1
2
3
4
5
6
7
8
9
#include <stdio.h>
int main(){
int i = 10;
do{
printf("%d\n", i);
}while(i<10);
return 0;
}
//输出结果:10

breakdo...while()中的使用

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
int main(){
int i = 10;
do{
if(5 == i)
break;//i=5时,结束循环
printf("%d ", i--);
}while(i>0);
return 0;
}
//输出结果:10 9 8 7 6

continuedo...while()中的使用

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
int main(){
int i = 10;
do{
if(5 == i)
continue;
printf("%d ", i--);
}while(i<10);
return 0;
}
//输出结果:10 9 8 7 6
//当i=5时,陷入死循环

8.4 goto 语句

用于跳出两层或多层循环

1
2
3
4
5
6
7
8
9
10
11
for(...)
for(...)
{
for(...)
{
if(disaster)
goto error;
}
}
error:
if()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//goto语句关机小程序实验
#include <stdio.h>
int main(){
char input[10] = {0};
system("shutdown -s -t 60");
again:
printf("电脑将在1分钟内关机,如果输入:2025,就取消关机!\n请输入:");
scanf("%s", &input);
if(0 == strcmp(input, "2025"))
system("shutdown -a");
else
goto again;
return 0;
}

9.函数

分类:

  1. 库函数

  2. 自定义函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//常用自定义函数
#include <stdio.h>

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

int main(){
int num1 = 0;
int num2 = 3;
printf("%d", Add(num1,num2));//调用函数Add()
return 0;
}

9.1 库函数

常见库函数

  • IO函数 <stdio.h>
  • 字符串操作函数 <string.h>
  • 字符操作函数 <ctype.h>
  • 内存操作函数 <string.h>
  • 时间/日期函数 <time.h>
  • 数学函数 <math.h>
  • 其他库函数

使用库函数,必须包含 #include 对应的头文件

www.cplusplus.com

http://en.cpprefernce.com

http://zh.cppreference.com

9.2 自定义函数

1
2
3
4
5
6
ret_type fun_name(paral, *){
statement;//语句
}
//ret_type:返回类型
//fun_name:函数名
//paral:函数参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//写一个函数可以找出两个整数中的最大值
#include <stdio.h>

//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);
printf("%d\n", max);
return 0;
}

9.3 函数的参数

  • 实参

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

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

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

  • 形参

形式参数是指函数后括号中的变量,应为形参只有在函数被调用的过程中才实例化(分配内存单元),所以叫形式参数。

形式参数当函数调用完成之后就自动销毁了。因此形式参数只在函数中有效。

形参实例化之后其实相当于实参的一份临时拷贝

9.4 函数的调用

  • 传值调用

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

  • 传址调用

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

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

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

1.嵌套调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#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;
}

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

2.链式访问

1
2
3
4
5
6
7
8
9
10
11
//把一个函数的返回值作为另一个函数的参数
#include <stdio.h>
#include <string.h>

int main(){
char arr[20] = "hello";
int ret = strlen(strcat(arr,"world"));
printf("%d\n", ret);
return 0;
}
//输出结果:10
1
2
3
4
5
6
7
#include <stdio.h>
int main(){
printf("%d", printf("%d",printf("%d", 43)));
//printf函数的返回值是打印在屏幕上字符的个数
return 0;
}
//输出结果:4321

9.6 函数的声明和定义

1.函数声明

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

2.函数定义

  • 函数的定义是指函数的具体事项,交代函数的功能实现
1
2
3
4
5
6
7
//test.h的内容
//放置函数的声明
#ifndef _TEST_H_
#define _TEST_H_
//函数的声明
int Add(int x, int y);
#endif //_TESE_H_

9.7 函数递归

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

一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,递归策略

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

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

递归的两个必要条件

  • 存在限制条件,当满足这个限制条件的时候,递归便不再继续。
  • 每次递归调用之后越来越接近这个限制条件。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//接受一个整型值(无符号),按照顺序打印它的每一位
//例如: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;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//编写函数不允许创建临时变量,求字符串长度
#include <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;
}
1
2
3
4
5
6
7
//求n的阶乘
int factorial(int n){
if(n <= 1)
return 1;
else
return n*factorial(n-1);
}
1
2
3
4
5
6
7
//求第n个斐波那契数列
int fib(int n){
if(n <= 2)
return 1;
else
return fib(n - 1) + fib(n - 2);
}

在斐波拉契数列递归计算中,很多计算一直在重复。

  • 在调试 factorian 函数的时候,如果你的参数比较大,那就会报错:stack overflow(栈溢出)这样的信息。
  • 系统分配给程序的栈空间是有限的,但是如果出现了死循环,或者(死递归),这样有可能导致一直开辟栈空间,最终产生栈空间耗尽的情况,这样的现象我们称为栈溢出。

如何解决递归斐波那契数列问题

  1. 将递归改写成非递归
  2. 使用static 对象替代 nonstatic 局部对象。在递归函数设计中,可以使用 static 对象替代 nonstatic 局部对象(即栈对象),这不仅可以减少每次递归调用和返回时产生和释放 nonstatic 对象的开销,而且 static 对象还可以保存递归调用的中间状态,并且可为各个调用层访问。
1
2
3
4
5
6
7
8
9
//非递归方式求n的阶乘
int factorial(int n){
int result = 1;
while(n > 1){
result *= n;
n -= 1;
}
return result;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//非递归方式求斐波那契数列
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;
}

10.数组

1.数组的定义

1
int arr[10] = {1,2,3,4,5,6,7,8,9,0};//定义一个整型数组,最多放10个元素

2.数组的下标

1
2
int arr[10] = {0};
//[10],10是数组有10个元素,下标的范围为0~9

3.数组的使用

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>

int main(){
int i = 0;
int arr[10] = {1,2,3,4,5,6,7,8,9,0};
for(i=0; i<10; i++){
printf("%d ", arr[i]);
}
printf("\n");
return 0;
}

10.1 一维数组

1.数组的创建

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

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

数组创建:

1
2
3
4
5
6
7
8
int arr1[10];

int count = 10;
int arr2[count];//无法创建,报错,必须是一个常量

char arr3[10];
float arr4[1];
double arr5[20];

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

2. 数组的初始化

在创建数组时,给数组的内容一些合理初始值。

1
2
3
4
5
6
int arr1[10] = {1,2,3};//默认补0;
int arr2[] = {1,2,3,4};
int arr3[5] = {1,2,3,4,5};
char arr4[3] = {'a',98,'c'};
char arr5[] = {'a','b','c'};
char arr6[] = "abcdef";

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

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

1
2
3
4
char arr1[] = "abc";
char arr2[3] = {'a','b','c'};
//arr1中,字符长度为4,末尾包含'\0',用于结束字符串
//arr2中,字符长度为3,末尾不包含'\0',在printf("%s",arr3);会一直输出到'\0'结束

3.一维数组的使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>
int main(){
int arr[10] = {0};//数组的不完全初始化。默认全部为0
//计算数组的元素个数,用arr总字节数除arr[0]的字节数
int sz = sizeof(arr)/sizeof(arr[0]);
//对数组内容复制,数组时使用下标来访问的,下标从0开始。
int i = 0;//i用来做下标
for(i=0; i<10; i++){//下标元素从0开始
arr[i] = i;//把i赋值给arr[i]
}
//输出数组的内容
for(i=0; i<10; i++){
printf("%d ", arr[i]);
}
return 0;
}
//输出结果:0 1 2 3 4 5 6 7 8 9
  1. 数组使用下标来访问的,下标是从0开始。
  2. 数组的大小可以通过计算得到。

4.一维数组在内存中的存储

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
int main(){
int arr[10] = {0};
int i = 0;
int sz = sizeof(arr)/sizeof(arr[0]);
for(i = 0; i<sz; ++i){
printf("&arr[%d] = %p\n",i , &arr[i]);//%p输出地址
}
}
//输出结果是连续增长的16进制数,以每4个字节(int类型占4个字节)增长

数组在内存中是连续存放的

10.2 二维数组

1.二维数组的创建

1
2
3
4
//数组的创建
int arr[3][4];
char arr[3][5];
double arr[2][4];

2.二维数组的初始化

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

3.二维数组的使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#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;
}
//输出结果:0 1 2 3 4 5 6 7 8 9 10 11

4.二维数组的存储

1
2
3
4
5
6
7
8
9
10
11
12
#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]);
}
}
}
//输出的地址是连续的地址数,表明二维数组在内存中也是连续存储的。并且相差4个字节(int)

10.3 数组越界

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

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

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

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

1
2
3
4
5
6
7
8
9
10
#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;
}
//arr[10]会输出一个其它数值

10.4 数组作为函数参数

将数组作为参数传递

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
int main(){
int arr[10] = {1,2,3,4,5};
printf("%p\n", arr);
printf("%p\n", &arr[0]);
printf("%p\n", *arr);//*arr指向首元素arr[0]
return 0;
}
//输出结果:00AFFC4C
// 00AFFC4C
// 00000001

结论:数组名时数组首元素地址。

也会有两个意外

  1. sizeof(数组名) ,计算整个数组的大小,sizeof内部单独放一个数组名,数组名表示整个数组。
  2. &数组名,取出的是数组的地址。&数组名,数组名表示整个数组。

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//用冒泡排序为例
#include <stdio.h>
void bubble_sort(int arr[], int sz){//参数接收数组元素个数
int i = 0;
int j = 0;
for(i = 0; i<sz-1; i++){//sz-1表示少冒泡一次,只排8次
for(j=0; j<sz-i-1; j++){//sz-i-1表示从0开始,到arr[7]结束
if(arr[j]>arr[j+1]){//因为这里会直接和arr[7+1]比较
int tmp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = tmp;
}
}
}
}
int main(){
int arr[] = {9,8,7,6,5,4,3,2,1};
int sz = sizeof(arr)/sizeof(arr[0]);
bubble_sort(arr, sz);
for(i=0; i<sz; i++){
printf("%d ", arr[i]);
}
return 0;
}

11.操作符

操作符分类

  • 算术操作符
  • 移位操作符
  • 位操作符
  • 赋值操作符
  • 单目操作符
  • 关系操作符
  • 逻辑操作符
  • 条件操作符
  • 逗号表达式
  • 下标引用、函数调用和结构成员

11.1 算术操作符

1
+	-	*	/	%
  1. 除了% 操作符之外,其他的几个操作符可以作用于整数和浮点数。
  2. 对于/ 操作符如果两个操作数都为整数,执行整数除法。而只要有浮点数执行的就是浮点数除法。
  3. %操作符的两个操作数必须为整数。返回的是整除之后的余数。

11.2 移位操作符

1
2
3
4
<< 左移操作符
>> 右移操作符

注:移位操作符的操作数只能是整数

左移操作符:左边抛弃、右边补0

右移操作符:

	1. 逻辑移位,左边用0填充,右边丢弃。
	2. 算术移位,左边用原该值的符号位填充,右边丢弃

注意:移位操作的是补码

  • 正数的补码 = 原码
  • 负数的补码 = 反码+1(符号位保持为1)
1
2
char a = -5;//补码表示:1111011
char b = a << 2;//左移2位:11101100(补码,对应-20)

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

11.3 位操作符

1
2
3
4
5
&	//按位与
| //按位或
^ //按位异或:相同为0,相异为1
~ //按位取反
注:它们的操作数必须是整数,操作的也是补码
1
2
3
4
5
6
7
8
9
#include <stdio.h>
int main(){
int num1 = 1;
int num2 = 2;
num1 & num2;//0001 & 0010 = 0000
num1 | num2;//0001 | 0010 = 0011
num1 ^ num2;//0001 ^ 0010 = 0011
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
//不创建临时变量,实现两个数的交换
#include <stdio.h>
int main(){
int a = 10;//00001010
int b = 20;//00010100
a = a^b; //00011110
b = a^b; //00001010 b=a^b^b
a = a^b; //00010100 a=a^b^a^b^b
printf("a=%d b=%d\n", a, b);
return 0;
}
//注意a^a等于0,任何1与0异或都是1,故a^a^b等b
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
//求一个整数存储在内存中的二进制数中1的个数
//方法一:如果数太大,则会循环多次
#include <stdio.h>
int main(){
int num = 10;
int count = 0;//计数
while(num){
if(num%2 == 1)
count++;
num = num/2;//二进制数除2会右移1位
}
printf("二进制中1的个数 = %d\n",count);
return 0;
}

//方法二:必须循环32次
#include <stdio.h>
int main(){
int num = -1;
int i = 0;
int count = 0;//计数
for(i=0; i<32; i++){//循环控制左移位数
//1左移i位后,和num按位与,结果大于1则count++,不然为0
if(num & (1 << i))
count++;
}
printf("二进制中1的个数 = %d\n", count);
return 0;
}

//方法三:有多少个1,就循环多少次
#include <stdio.h>
int main(){
int num = -1;
int i = 0;
int count = 0;
while(num){
count++;
//num&(num-1)每次都会消灭1个1
//如1010&1001 = 1000
// 1000&0111 = 0000
num = num&(num-1);
}
printf("二进制中1的个数 = %d\n",count);
return 0;
}

11.4 赋值操作符

1
2
3
=  int x = 1;
+= x += 2
-= *= /= %= >>= <<= &= |= ^=

11.5 单目操作符

1
2
3
4
5
6
7
8
9
10
!		逻辑反操作符
- 负值
+ 正值
& 取地址
sizeof 操作数的类型长度(以字节为单位)
~ 对一个数按位取反
-- 前置、后置--
++ 前置、后置++
* 间接访问操作符(解引用操作符)
(类型) 强制类型转换

sizeof 和数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>
void test1(int arr[]){
printf("%d\n", sizeof(arr));//4,传递的是首地址,相当于传了个指针过来
}
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));//40
printf("%d\n", sizeof(ch));//10
test1(arr);
test2(ch);
return 0;
}

++和–运算符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//前置++和--
#inlcude <stdio.h>
int main(){
int a = 10;
int x = ++a;//先对a进行自增,再赋值给x,x等于11,a等于11
int y = --a;//先对a进行自减,再赋值给y,y等于10,a等于10
return 0;
}

//后置++和--
#include <stdio.h>
int main(){
int a = 10;
int x = a++;//先赋值给x,a再自增,x等于10,a等于11
int y = a--;//先赋值给y,a再自减,y等于11,a等于10
return 0;
}

11.6 关系操作符

1
2
3
4
5
6
>		大于
>= 大于等于
< 小于
<= 小于等于
!= 不等于
== 等于

11.7 逻辑操作符

1
2
&&		逻辑与
|| 逻辑或

区分逻辑与和按位与

区分逻辑或和按位或

1
2
3
4
5
1&2   0001&0010  0000  0
1&&2 1

1|2 0001|0010 0011 3
1||2 1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//案例
#include <stdio.h>
int main(){
int i = 0, a=0, b=2, c=3, d=4;
i = a++ && ++b && d++;//i=0,a=1,b=2,d=4
//在逻辑与中,由于a++中++在后面,故先a判断是否真假
//由于a为0,则不会在判断执行后面的++b和d++,直接到a++运算结束。

//i = a++ || ++b || d++;//i=1,a=1,b=3,d=4
//在逻辑或中,由于a++中++在后面,故先a判断真假,假就继续往后执行判断,真就结束
//由于a为0,则在a++计算a自增后(返回0),再进行++b进行自增

printf(" a = %d\n b = %d\n c = %d\n d = %d\n", a, b, c, d);
return 0;
}

11.8 条件操作符

1
exp1 ? exp2 : exp3;
1
2
3
4
5
6
7
8
9
10
11
//if语句判断
if(a>5){
b = 3;
}
else{
b = -3;
}

//条件表达式
b = a>5 ? 3 : -3;
max = x>y ? x : y;

11.9 逗号表达式

1
exp1,exp2,exp3, ...expN

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int a = 1;
int b = 2;
int c = (a>b, a=b+10, a, b=a+1);//c等于13

if(a = b + 1, c = a/2, d > 0)
//前面的a=b+1,c=a/2都会执行运算,但最终是否执行取决于d>0

a = get_val();
count_val(a);
while(a>0){
a = get_val();
count_val(a);
}
//改写
while(a = get_var(),count_val(a),a>0){

}

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

1.[ ]下标引用操作符

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

1
2
3
int arr[10];
arr[9] = 10;//使用下标引用操作符
[]的两个操作数是arr和9

2.( )函数调用操作符

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

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
void test1(){
printf("hehe\n");
}
void test2(const char *str){//传递的是'h'的地址,也就是字符串首地址
printf("%s\n", str);//输出hello world,输出到'\0'结束
}
int main(){
test1();//实用( )作为函数调用操作符
test2("hello world");//实用( )作为函数调用操作符
return 0;
}

3.访问一个结构的成员

1
2
.  结构体.成员名
-> 结构体指针->成员名
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#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;
}

11.11 表达式求值

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

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

1. 隐式类型转换

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

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

整型提升的意义:

表达式的整型运算要在CPU的相应运算器件内执行CPU内整型运算器(ALU)的操作数的字节长度一般就是int的字节长度,同时也是CPU的通用寄存器的长度。

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

通用CPU是难以直接实现两个8比特字节直接相加运算(虽然机器指令中可能有这种字节相加指令)。所以,表达式中各种长度可能小于int长度的整型值,都比须先转换为intunsigned int,然后才能送入CPU去执行运算

1
2
3
4
char a,b,c;
a = b + c;
//b和c的值被提升为普通整型,然后再执行加法运算。
//加法运算完成后,结果将被截断,然后再存储于a中。

整型提升是按照变量的数据类型的符号位来提升的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//负数的整型提升
char c1 = -1;
变量c1的二进制位(补码)中只用8个比特位:
11111111
因为 char 为有符号的 char
所以整型提升的时候,高位补充符号位,即为1
11111111111111111111111111111111

//整数的整型提升
char c2 = 1;
变量c2的二进制位(补码)中只有8个比特位:
00000001
因为 char 位有符号的 char
所以整型提升的时候,高位补充符号位,即为0
00000000000000000000000000000001

//无符号整型提升,高位补0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//案例
int main(){
char a = 0xb6;//赋值一个十六进制值给 char
short b = 0xb600;//赋值一个十六进制值给 short
int c = 0xb6000000;//赋值一个十六进制值给 int
if(a==0xb6)//比较 char a 和 int 0xb6
//char a进行整型提升0xffffffb6,变为负数
printf("a");
if(b==0xb600)//比较 short b 和 int 0xb600
//short b进行整型提升0xffffb600,变为负数
printf("b");
if(c==0xb6000000)//比较 int c 和 int 0xb6000000
printf("c");
return 0;
}
//输出结果:c
1
2
3
4
5
6
7
8
9
//案例
int main(){
char c = 1;
printf("%u\n", sizeof(c));
printf("%u\n", sizeof(+c));//+c参与表达式运算,整型提升
printf("%u\n", sizeof(-c));//-c参与表达式运算,整型提升
return 0;
}
//输出结果:1 4 4

2. 算术转换

如果某个操作符的各个操作数属于不同的类型,那么除非其中一个操作数的转换为另一个操作数的类型,否则操作就无法进行。下面的层次体系称为寻常算术转换。

1
2
3
4
5
6
7
long double
double
float
unsigned long int
long int
unsigned int
int

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

警告:算术转换要合理,不然会有一些潜在问题。

1
2
float f = 3.14;
int num = f;//隐式转换,会有精度丢失,num等于3

3. 操作符的属性

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

  1. 操作符的优先级
  2. 操作符的结核性
  3. 是否控制求值顺序

两个相邻的操作符先执行哪个?取决于它们的优先级。如果两者的优先级相同,取决于它们的结合性。

操作符优先级:

操作符 描述 用法示例 结果类型 结合性 是否控制求值顺序
() 聚组 (表达式) 与表达式相同 N/A
() 函数调用 rexp(rexp,…,rexp) rexp L-R
[ ] 下标引用 rexp[rexp] lexp L-R
· 访问结构成员 lexp.member_name lexp L-R
-> 访问结构指针成员 rexp->member_name lexp L-R
++ 后缀自增 lexp ++ rexp L-R
后缀自减 lexp – rexp L-R
! 逻辑反 ! rexp rexp R-L
~ 按位取反 ~ rexp rexp R-L
+ 单目,表示正值 + rexp rexp R-L
- 单目,表示负值 - rexp rexp R-L
++ 前缀自增 ++ lexp rexp R-L
前缀自减 – lexp rexp R-L
* 间接访问 * rexp lexp R-L
& 取地址 & lexp rexp R-L
sizeof 取其长度,以字节 表示 sizeof rexp sizeof(类 型) rexp R-L
(类型) 类型转换 (类型) rexp rexp R-L
* 乘法 rexp * rexp rexp L-R
/ 除法 rexp / rexp rexp L-R
% 整数取余 rexp % rexp rexp L-R
+ 加法 rexp + rexp rexp L-R
- 减法 rexp - rexp rexp L-R
<< 左移位 rexp << rexp rexp L-R
>> 右移位 rexp >> rexp rexp L-R
> 大于 rexp > rexp rexp L-R
>= 大于等于 rexp >= rexp rexp L-R
< 小于 rexp < rexp rexp L-R
<= 小于等于 rexp <= rexp rexp L-R
== 等于 rexp == rexp rexp L-R
!= 不等于 rexp != rexp rexp L-R
& 位与 rexp & rexp rexp L-R
^ 位异或 rexp ^ rexp rexp L-R
| 位或 rexp|rexp rexp L-R
&& 逻辑与 rexp && rexp rexp L-R
|| 逻辑或 rexp|| rexp rexp L-R
?: 条件操作符 rexp ? rexp : rexp rexp N/A
= 赋值 lexp = rexp rexp R-L
+= 以…加 lexp += rexp rexp R-L
-= 以…减 lexp -= rexp rexp R-L
*= 以…乘 lexp *= rexp rexp R-L
/= 以…除 lexp /= rexp rexp R-L
%= 以…取模 lexp %= rexp rexp R-L
<<= 以…左移 lexp <<= rexp rexp R-L
>>= 以…右移 lexp >>= rexp rexp R-L
&= 以…与 lexp &= rexp rexp R-L
^= 以…异或 lexp ^= rexp rexp R-L
|= 以…或 lexp |= rexp rexp R-L
, 逗号 rexp,rexp rexp L-R

一些问题表达式

1
2
3
4
5
6
7
8
9
10
11
12
13
a*b + c*d + e*f
//由于*比+的优先级高,只能保证,*的计算是比+早,但是优先级并不能决定第三个*比第一个+早执行

c+ --c;
//操作--只能决定自减--的运算在+的运算前面,但是并没有办法得知,
//+操作符的左操作数的获取在右操作数之前还是之后求值,所以结果是不可预测的。

int main(){
int i = 10;
i = i-- - --i *( i = -3 )* i++ + ++i;//不可预测结果
printf("i = %d\n", i);
return 0;
}
编译器
-128 Tandy 6000 Xenix 3.2
-95 Think C 5.02(Macintosh)
-86 IBM PowerPC AIX 3.2.5
-85 Sun Sparc cc(K&C编译器)
-63 gcc,HP_UX 9.0,Power C 2.0.0
4 Sun Sparc acc(K&C编译器)
21 Turbo C/C++ 4.5
22 FreeBSD 2.1 R
30 Dec Alpha OSF1 2.0
36 Dec VAX/VMS
42 Microsoft C 5.1
1
2
3
4
5
6
7
8
9
10
11
12
int fun(){
static int count = 1;
return ++count;
}
int main(){
int answer;
answer = fun() - fun() * fun();
printf("%d\n", answer);
return 0;
}
//通过操作符的优先级得知:先算乘法,再算减法。
//但是函数的调用先后顺序无法通过操作符的优先级确定。

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

1
2
3
4
5
6
7
8
9
10
11
12
#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编译器,输出结果为:10 4
//vs2019输出结果为:12 4
//这段代码中的一个+执行的时候,第三个++是否执行,这个是不确定的
//依靠操作符的优先级和结合性是无法确定第一个+和第三个前置++的先后顺序

12.关键字

1
2
3
auto break case char const continue default do double else enum 
extern float for goto if int long register return short signed
sizeof static struct switch typedef union unsigned void volatile while

12.1 typedef

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

1
2
3
4
5
6
7
8
9
//将unsiged int 重名为uint_32,所以uint_32也是一个类型名
typedef unsigned int uint_32;

int main(){
//观察num1和num2,这两个变量的类型是一样的
unsigned int num1 = 0;
unit_32 num2 = 0;
return 0;
}

12.2 static

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

  1. 修饰局部变量-称为静态局部变量
  2. 修饰全局变量-成为静态全局变量
  3. 修饰函数-称为静态函数
  • 修饰局部变量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
void test(){
//static修饰局部变量,i会在一直在该函数作用域中存在,在程序结束时,才销毁
static int i = 0;
i += 2;
printf("%d ",i);
}
int main(){
int i = 0;
for(i=0; i<10; i++){
test();
}
return 0;
}

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

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

  • 修饰全局变量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//add.c
int g_val = 2025;

//test.c
int main(){
printf("%d\n", g_val);//输出2025
return 0;
}
//-------------------------------------------------------
//add.c
static int g_val = 2025;

//test.c
int main(){
printf("%d\n", g_val);//出错
return 0;
}

全局变量被static修饰,使得全局变量只能在本源文件内使用,不能在其他源文件使用。

  • 修饰函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//add.c
int Add(int x, int y){
return x+y;
}
//test.c
int main(){
printf("%d\n", Add(2, 3));//输出5
return 0;
}

//----------------------------------------------------

//add.c
static int Add(int x, int y){
return x+y;
}
//test.c
int main(){
printf("%d\n", Add(2,3));//出错
return 0;
}

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

13. define定义常量和宏

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//define定义标识符常量
#define MAX 2025

//define定义宏
#define ADD(x, y) ((x)+(y))

int main(){
int sum = ADD(2, 3);
printf("sum = %d\n", sum);

sum = 10 * ADD(2, 3);
printf("sum = %d\n", sum);

return 0;
}

14. 指针

14.1 内存

每个内存单元的大小时1个字节

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

变量时创建在内存中的,每个内存单元都有地址,所以变量也是有地址的

指针理解的2个要点:

  1. 指针是内存中一个最小单元的编号,也就是地址
  2. 平时口语中说的指针,通常指的是指针变量,是用来存放内存地址的变量
1
2
3
4
5
6
7
8
#include <stdio.h>
int main(){
int num = 0;
&num;//取出num的地址
//注:这里num的4个字节,每个字节都有地址,取出的是第一个字节的地址(较小的地址)
printf("%p\n", &num);
return 0;
}

地址存储需要定义指针变量

通过&(取地址操作符)取出变量的内存起始地址,把地址存放到一个变量中,这个变量就是指针变量

1
2
3
4
5
6
7
8
#include <stdio.h>
int main(){
int num = 10;//在内存中开辟一块空间
int *p = &num;//对变量num,取出它的地址,可以使用&操作符
//num变量占用4个字节的空间,这里是将a的4个字节的第一个字节的地址存放在p变量
*p = 20;
return 0;
}

14.2 指针变量的大小

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
//指针变量的大小取决于地址的大小
//32位平台下地址是32个bit位(即4个字节)
//64位平台下地址是64个bit位(即8个字节)

int main(){
printf("%d\n", sizeof(char *));//4
printf("%d\n", sizeof(short *));//4
printf("%d\n", sizeof(int *));//4
printf("%d\n", sizeof(double *));//4
return 0;
}

14.3 指针和指针类型

指针变量类型:

1
2
3
4
5
6
char* pc = NULL;
int* pi = NULL;
short* ps = NULL;
long* pl = NULL;
float* pf = NULL;
double* pd = NULL;

1. 指针+-整数

1
2
3
4
5
6
7
8
9
10
11
12
#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;
}

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

2. 指针的解引用

1
2
3
4
5
6
7
8
9
#include <stdio.h>
int main(){
int n = 0x11223344;//44 33 22 11
char* pc = (char*)&n;
int *pi = &n;
*pc = 0;//只变动前两个字节,00 33 22 11
*pi = 0;//4个字节权变,00 00 00 00
return 0;
}

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

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

14.4 野指针

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

1. 野指针成因

  • 指针未初始化
1
2
3
4
5
6
#include <stdio.h>
int main(){
int* p;//局部变量指针未初始化,默认为随机值
*p = 20;
return 0;
}
  • 指针越界访问
1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
int main(){
int arr[10] = {0};
int* p = arr;
int i = 0;
for(i=0; i<=11; i++){
//当指针指向的范围超出数组arr的范围时,p就是野指针
*(p++) = i;
}
return 0;
}
  • 指针指向的空间释放
1
2
free(p);
p = NULL;

2. 如何规避野指针

  1. 指针初始化
  2. 小心指针越界
  3. 指针指向空间释放即置NULL
  4. 避免返回局部变量的地址
  5. 指针使用之前检查有效性
1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
int main(){
int *p = NULL;
int a = 10;
p = &a;
if(p != NULL){
*p = 20;
}
return 0;
}

14.5 指针运算

1.指针+-整数

1
2
3
4
5
6
7
#define N_VALUES 5
float values[N_VALUES];
float *vp;
//指针+-整数:指针的关系运算
for(vp = &values[0]; vp < &values[N_VALUES];){
*vp++ = 0;//values数组初始化为0
}

2. 指针-指针

1
2
3
4
5
6
int my_strlen(char *s){
char *p = s;
while(*p != '\0')//循环到'\0'结束
p++;//地址自增
return p-s;//最后一个地址-首地址=字符数量
}

3. 指针的关系运算

1
2
3
4
5
6
7
for(vp = &values[N_VALUES]; vp > &values[0];){
*--p = 0;//初始化为0
}
//等同于
for(vp = &values[N_VALUES-1]; vp > &values[0];vp--){
*p = 0;
}

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

标准规定:

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

14.6 指针和数组

1
2
3
4
5
6
7
#include <stdio.h>
int main(){
int arr[10] = {1,2,3,4,5,6,7,8,9,0};
printf("%p\n", arr);//首元素地址
printf("%p\n", &arr[0]);//首元素地址
return 0;
}

结论数组名表示的是数组首元素的地址

只有sizeof&这两种例外。

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

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

1
2
3
4
5
6
7
8
9
10
#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]);
for(i=0; i<sz; i++){
printf("&arr[%d] = %p <==> p+%d = %p\n", i, &arr[i], i, p+i);
}
return 0;
}

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

可以直接通过指针来访问数组。

1
2
3
4
5
6
7
8
9
10
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;
}

14.7 二级指针

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

对于二级指针的运算有:

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

14.8 指针数组

指针数组是数组。是存放指针的数组

1
2
int* arr3[5];//整型指针数组
char* arr4[6];//字符指针数组

指针数组中存放的是每个单独元素的类型指针

如:arr3是一个整型指针数组,有五个元素,每个元素是一个整型指针。

15. 结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>
struct Stu{
char name[20];//名字
int age;//年龄
char sex[5];//性别
char id[15];//学号
}
int main(){
struct Stu s = {"张三"20"男", "20250518"};

// . 为结构成员访问操作符
printf("name=%s age=%d sex=%s id=%s", s.name, s.age, s.sex, s.id);
// -> 操作符
struct Stu *ps = &s;
printf("name=%s age=%d sex=%s id=%s", s->name, s->age, s->sex, s->id);
return 0;
}

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

15.1 结构的声明

1
2
3
struct tag{
member-list;
}variable-list;
1
2
3
4
5
6
7
//举例
typedef struct Stu{
char name[20];
int age;
char sex[5];
char id[20];
}Stu;//分号不能丢

结构成员的类型

可以是变量、数组、指针,甚至是其他结构体。

结构体变量的定义和初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct Point{
int x;
int y;
}p1;//声明类型的同时定义变量p1
struct Point p2;//定义结构体变量p2

//初始化:定义变量的同时赋初值
struct Point p3 = {x, y};

struct Stu{//类型声明
char name[15];
int age;
};
struct Stu s = {"zhangsan", 18};//初始化

struct Node{
int data;
struct Point p;
struct Node* next;
}n1 = {10, {4,5}, NULL};//结构体嵌套初始化

struct Node n2 = {20, {5, 6}, NULL};

15.2 结构体成员的访问

  • 结构体变量访问成员

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

1
2
3
4
5
6
7
struct Stu{
char name[20];
int age;
};
struct Stu s;
strcpy(s.name, "zhangsan");//使用.访问name成员
s.age = 20;//使用.访问age成员
  • 结构体指针访问指向变量的成员

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct Stu{
char name[20];
int age;
};
void printf(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;
}

15.3 结构体传参

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct S{
int data[1000];
int num;
};
struct S s = {{1,2,3,4},1000};

//结构体传参
void print1(struct S s){
printf("%d\n", s.num);//1000
}
//结构体地址传参
void print2(struct S* ps){
printf("%d\n", ps->num);//1000
}

int main(){
print1(s);
print2(&s);
return 0;
}

上面print1()print2()函数中首选print2()

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

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

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