一、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 ; }
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 5 6 7 8 9 10 #include <stdio.h> int global = 2025 ;int main () { int local = 2024 ; int global = 2020 ; printf ("%d" , global); 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) {} int main () { 3.14 ; 2025 ; const int con = 2025 ; con = 2024 ; const int * p1; int * 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); printf ("%s\n" , arr2); printf ("%s\n" , arr3); 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 { }
1 2 3 4 int num = 1 ;if (5 == num){ 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 ; 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++; case 2 : n++; case 3 : switch (n){ case 1 : n++; case 2 : m++; n++; break ; } case 4 : m++; break ; default : break ; } printf ("m = %d, n = %d\n" , m, n); return 0 ; }
8.循环语句
while语句
for 语句
do …while()语句
8.1 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 ) break ; printf ("%d " ,i); i = i+1 ; } return 0 ; }
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 ) continue ; printf ("%d " ,i); i = i+1 ; } return 0 ; }
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 ; }
continue 在while循环中的作用:
continue是用于终止本次循环的,也就是本次循环中continue后边的代码不会再执行,直接跳转到while语句的判断部分。进行下一次循环的入口判断。
8.2 for循环 1 2 3 4 5 for (表达式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 ; }
break 在for
的使用
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 ; printf ("%d " , i); } return 0 ; }
continue 在for
中的使用
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 ; printf ("%d " , i); } return 0 ; }
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 (;;){ printf ("hehe\n" ); } int i = 0 ; int j = 0 ; for (; i < 10 ; i++){ for (; j < 10 ; j++){ printf ("hehe\n" ); } } 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 ; }
8.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 ; }
break 在do...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 ; printf ("%d " , i--); }while (i>0 ); return 0 ; }
continue 在do...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 ; }
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 #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 3 4 5 6 7 8 9 10 11 12 13 14 #include <stdio.h> int Add (int x, int y) { return x+y; } int main () { int num1 = 0 ; int num2 = 3 ; printf ("%d" , Add(num1,num2)); 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; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #include <stdio.h> 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 ; }
1 2 3 4 5 6 7 #include <stdio.h> int main () { printf ("%d" , printf ("%d" ,printf ("%d" , 43 ))); return 0 ; }
9.6 函数的声明和定义 1.函数声明
告诉编译器有一个函数叫什么,参数是什么,返回类型是什么。但是具体是不是不存在,函数生命决定不了。
函数的声明一般出现在函数的使用之前。要满足先生命后使用。
函数的声明一般放在头文件中。
2.函数定义
1 2 3 4 5 6 7 #ifndef _TEST_H_ #define _TEST_H_ int Add (int x, int y) ;#endif
9.7 函数递归
程序调用自身的编程技巧被称为递归(recuresion)。
一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,递归策略
只需要少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量
递归的主要思考方式在于:把大事化小
递归的两个必要条件
存在限制条件,当满足这个限制条件的时候,递归便不再继续。
每次递归调用之后越来越接近这个限制条件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #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 int factorial (int n) { if (n <= 1 ) return 1 ; else return n*factorial(n-1 ); }
1 2 3 4 5 6 7 int fib (int n) { if (n <= 2 ) return 1 ; else return fib(n - 1 ) + fib(n - 2 ); }
在斐波拉契数列递归计算中,很多计算一直在重复。
在调试 factorian
函数的时候,如果你的参数比较大,那就会报错:stack overflow
(栈溢出)这样的信息。
系统分配给程序的栈空间是有限的,但是如果出现了死循环,或者(死递归),这样有可能导致一直开辟栈空间,最终产生栈空间耗尽的情况,这样的现象我们称为栈溢出。
如何解决递归斐波那契数列问题
将递归改写成非递归
使用static
对象替代 nonstatic
局部对象。在递归函数设计中,可以使用 static
对象替代 nonstatic
局部对象(即栈对象),这不仅可以减少每次递归调用和返回时产生和释放 nonstatic
对象的开销,而且 static
对象还可以保存递归调用的中间状态,并且可为各个调用层访问。
1 2 3 4 5 6 7 8 9 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 };
2.数组的下标
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];
数组创建:
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 };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' };
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 }; int sz = sizeof (arr)/sizeof (arr[0 ]); int i = 0 ; for (i=0 ; i<10 ; i++){ arr[i] = i; } for (i=0 ; i<10 ; i++){ printf ("%d " , arr[i]); } return 0 ; }
数组使用下标来访问的,下标是从0开始。
数组的大小可以通过计算得到。
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]); } }
数组在内存中是连续存放的
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 ; }
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]); } } }
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]); } return 0 ; }
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); return 0 ; }
结论:数组名时数组首元素地址。
也会有两个意外
sizeof(数组名)
,计算整个数组的大小,sizeof
内部单独放一个数组名,数组名表示整个数组。
&数组名
,取出的是数组的地址。&
数组名,数组名表示整个数组。
除上述两种情况之外,所有的数组名都表示数组首元素的地址。
当数组传参的时候,实际上只是把数组的首元素的地址传递过去了。
所以即使在函数参数部分写成数组的形式: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++){ 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 () { 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 算术操作符
除了%
操作符之外,其他的几个操作符可以作用于整数和浮点数。
对于/
操作符如果两个操作数都为整数,执行整数除法。而只要有浮点数执行的就是浮点数除法。
%
操作符的两个操作数必须为整数。返回的是整除之后的余数。
11.2 移位操作符 1 2 3 4 << 左移操作符 >> 右移操作符 注:移位操作符的操作数只能是整数
左移操作符:左边抛弃、右边补0
右移操作符:
1. 逻辑移位,左边用0填充,右边丢弃。
2. 算术移位,左边用原该值的符号位填充,右边丢弃
注意:移位操作的是补码
正数的补码 = 原码
负数的补码 = 反码+1(符号位保持为1)
1 2 char a = -5 ;char b = a << 2 ;
警告:对于移位运算符,不要移动负数位,这个是标准未定义的。
11.3 位操作符 1 2 3 4 5 & | ^ ~ 注:它们的操作数必须是整数,操作的也是补码
1 2 3 4 5 6 7 8 9 #include <stdio.h> int main () { int num1 = 1 ; int num2 = 2 ; num1 & num2; num1 | num2; num1 ^ num2; return 0 ; }
1 2 3 4 5 6 7 8 9 10 11 12 #include <stdio.h> int main () { int a = 10 ; int b = 20 ; a = a^b; b = a^b; a = a^b; printf ("a=%d b=%d\n" , a, b); return 0 ; }
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 #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 ; } #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 ; } #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 ; }
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)); } void test2 (char ch[]) { printf ("%d\n" , sizeof (ch)); } int main () { int arr[10 ] = {0 }; char ch[10 ] = {0 }; printf ("%d\n" , sizeof (arr)); printf ("%d\n" , sizeof (ch)); 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; int y = --a; return 0 ; } #include <stdio.h> int main () { int a = 10 ; int x = a++; int y = a--; return 0 ; }
11.6 关系操作符 1 2 3 4 5 6 > 大于 >= 大于等于 < 小于 <= 小于等于 != 不等于 == 等于
11.7 逻辑操作符
区分逻辑与和按位与
区分逻辑或和按位或
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++; printf (" a = %d\n b = %d\n c = %d\n d = %d\n" , a, b, c, d); return 0 ; }
11.8 条件操作符
1 2 3 4 5 6 7 8 9 10 11 if (a>5 ){ b = 3 ; } else { b = -3 ; } b = a>5 ? 3 : -3 ; max = x>y ? x : y;
11.9 逗号表达式
逗号表达式,就是用逗号隔开多个表达式。
逗号表达式,从左向右依次执行。整个表达式的结果是最后一个表达式的结果。
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 );if (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) { printf ("%s\n" , str); } int main () { test1(); test2("hello world" ); return 0 ; }
3.访问一个结构的成员
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
长度的整型值,都比须先转换为int
或unsigned int
,然后才能送入CPU去执行运算
1 2 3 4 char a,b,c;a = b + c;
整型提升是按照变量的数据类型的符号位来提升的
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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 2 3 4 5 6 7 8 9 int main () { char c = 1 ; printf ("%u\n" , sizeof (c)); printf ("%u\n" , sizeof (+c)); printf ("%u\n" , sizeof (-c)); return 0 ; }
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;
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 ; }
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 typedef unsigned int uint_32;int main () { unsigned int num1 = 0 ; unit_32 num2 = 0 ; return 0 ; }
12.2 static
static是用来修饰变量和函数的
修饰局部变量-称为静态局部变量
修饰全局变量-成为静态全局变量
修饰函数-称为静态函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 #include <stdio.h> void test () { 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 int g_val = 2025 ;int main () { printf ("%d\n" , g_val); return 0 ; } static int g_val = 2025 ;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 int Add (int x, int y) { return x+y; } int main () { printf ("%d\n" , Add(2 , 3 )); return 0 ; } static int Add (int x, int y) { return x+y; } 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 MAX 2025 #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 3 4 5 6 7 8 #include <stdio.h> int main () { int num = 0 ; # printf ("%p\n" , &num); return 0 ; }
地址存储需要定义指针变量
通过&(取地址操作符)取出变量的内存起始地址,把地址存放到一个变量中,这个变量就是指针变量
1 2 3 4 5 6 7 8 #include <stdio.h> int main () { int num = 10 ; int *p = # *p = 20 ; return 0 ; }
14.2 指针变量的大小 1 2 3 4 5 6 7 8 9 10 11 12 #include <stdio.h> int main () { printf ("%d\n" , sizeof (char *)); printf ("%d\n" , sizeof (short *)); printf ("%d\n" , sizeof (int *)); printf ("%d\n" , sizeof (double *)); 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 ; char * pc = (char *)&n; int *pi = &n; *pc = 0 ; *pi = 0 ; 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++){ *(p++) = i; } return 0 ; }
2. 如何规避野指针
指针初始化
小心指针越界
指针指向空间释放即置NULL
避免返回局部变量的地址
指针使用之前检查有效性
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 ; }
2. 指针-指针
1 2 3 4 5 6 int my_strlen (char *s) { char *p = s; while (*p != '\0' ) p++; return p-s; }
3. 指针的关系运算
1 2 3 4 5 6 7 for (vp = &values[N_VALUES]; vp > &values[0 ];){ *--p = 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;
既然可以把数组名当成地址存放到一个指针中,我们使用指针来访问就成为可能。
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
**ppa
先通过*ppa
找到pa
,然后对pa
进行解引用操作:*pa
,那找到的是a
1 2 3 4 5 6 int a = 10 ;int *pa = &a;int **ppa = &pa;**ppa = 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; struct Point 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" );s.age = 20 ;
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); } void print2 (struct S* ps) { printf ("%d\n" , ps->num); } int main () { print1(s); print2(&s); return 0 ; }
上面print1()
和print2()
函数中首选print2()
函数传参的时候,参数是需要压栈的。
如果传递一个结构体对象的时候,结构体过大,参数压栈的系统开销比较大,所以会导致性能的下降。
结论:结构体传参的时候,要传结构体的地址。