目录
一、初识c语言
1、什么是C语言?
C语言是一门面向过程的计算机编程语言,广泛应用于底层开发,与C++、java等面向对象的编程语言有所不同。Cyuyan的设计目标是提供一种能以简易的方式编译、处理低级存储器、产生少量的机器码以及不需要任何运行环境支持便能运行的编程语言。
2、第一个C语言程序
#include<stdio.h> //头文件
int main()//main函数为程序入口,一个工程中main函数仅有一个
{
printf("hello\n");
return 0;//return函数结束,后面不执行
}
3、数据类型
数据类型 | 打印方式 | |
char | 字符 | %c |
short | 短整型 | %d |
int | 整型 | %d |
long | 长整型 | %d/%ld |
float | 单精度浮点数 | %f |
double | 双精度浮点数 | %lf |
注:打印八进制数(%0),打印十六进制数(%0x)
4、变量、常量
(1)变量的作用域和生命周期
- 作用域
a、局部变量作用域为变量所在的局部范围。
b、全局变量作用域为整个工程。
- 生命周期
变量对应的生命周期为该变量的作用域。
(2)C语言中常量分类
- 字面常量
- const修饰的常变量
- #define定义的标识符常量
- enum枚举常量(定义常量时,默认从0开始)
5、转义字符
转义字符 | 释义 |
\' | 表示字符常量' |
\" | 表示字符串内部的双引号 |
\\ | 表示反斜杠 |
\b | 退格 |
\n | 换行 |
\r | 回车 |
\ddd | ddd表示1~3个八进制数字 |
\xdd | dd表示两个十六进制数字 |
6、注释
- C语言注释风格/*xxxxxxx*/(缺陷:不能嵌套使用)
- C++注释风格 //xxxxxxx
注:几个常用快捷键
Ctrl+k+f //整理代码格式
Ctrl+k+c //整体注释
Ctrl+k+u //取消注释
二、选择与循环语句
1、选择语句(if和switch语句)
(1)if语句
“=”表示赋值,“==”判断相等,“!=”判断不相等。
//多分支情况
if (表达式)
{
语句一;
}
else if (表达式)
{
语句二;
}
else
{
语句三;
}
如果表达式为真,则语句执行。
注:在C语言中如何表示真假呢?
0表示真,非0表示假。
(2)switch语句(常用于多分支情况)
比如:
输入1:输出星期一
输入2:输出星期二
输入3:输出星期三
输入4:输出星期四
输入5:输出星期五
输入6:输出星期六
输入7:输出周日
如果用if语句的话会显得很麻烦,我么们可以用switch语句来实现。
#include<stdio.h>
int main()
{
int day = 0;
scanf("%d", &day);
switch (day)//条件只能是整型
{
case 1: //case后面只能是整型常量
printf("星期一\n");
break;
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;
}
- 在switch语句中,没法直接实现分支,搭配break使用才能实现真正的分支。break语句的实际效果是把语句列表划分为不同的部分。
- default子句:在switch表达式的值不匹配所有的case标签的值时,default字句后面的语句就会执行。所以,每个switch语句中只能出现一条default子句。
2、循环语句(while、for、do while)
(1)while循环
在屏幕上打印1-10的数字
#include<stdio.h>
int main()
{
int i = 1;
while (i <= 10)
{
printf("%d ", i);
i++;
}
return 0;
}
while循环里的break和continue
- break:循环中只要遇到break,就停止后期的所有循环,直接终止循环。所以:while中的break是用来永久终止循环的。
- continue:用于终止本次循环,也就是本次循环中continue后面的代码不会再执行,而是直接跳转到while语句的判断部分,进入下一次循环判断。
(2)for循环
for(表达式一;表达式二;表达式三)
循环语句;
表达式一用于初始化循环变量。表达式二条件判断部分,用于判断循环是否终止。表达式三用于循环条件的调整。
使用for循环在屏幕上打印1-10数字
#include<stdio.h>
int main()
{
int i = 0;
//for (i = 1/*初始化*/; i <= 10/*判断部分*/; ++i/*调整部分*/)
for (i = 1; i <= 10; ++i)
{
printf("%d ", i);
}
return 0;
}
注:1、不可在for循环内修改循环变量,防止for循环失去控制。
2、建议for语句的循环控制变量的取值采用前闭后开区间写法。
(3)do……while()循环
do
循环语句;
while(表达式);
特点:循环至少执行一次,适用的场景有限,所以不是经常使用。
三、函数
C语言函数分类:库函数、自定义函数
1、C语言常用库函数
- IO函数
- 字符串操作函数
- 内存操作函数
- 时间/日期函数
- 数学函数
- 其他库函数
注: 但是库函数必须知道的一个秘密就是:使用库函数,必须包含 #include 对应的头文件。
2、自定义函数
ret_type fun_name(para1, * )//ret_type返回类型,fun_name函数名,para1函数参数
{
statement;//语句项
}
举一个例子:
写一个函数可以找出两个整数中的最大值。
#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("max = %d\n", max);
return 0;
}
3、函数参数
形式参数(形参):
真实传给函数的参数,叫实参。实参可以是:常量、变量、表达式、函数等。无论实参是何种类型的量,在进行函数调用时,它们都必须有确定的值,以便把这些值传送给形参。
实际参数(实参):
形式参数是指函数名后括号中的变量,因为形式参数只有在函数被调用的过程中才实例化(分配内存单元),所以叫形式参数。形式参数当函数调用完成之后就自动销毁了。因此形式参数只在函数中有效。
上面代码x、y为形参,num1、num2为实参。形参实例化之后其实相当于实参的一份临时拷贝。
4、函数的声明和定义
(1)函数声明
- 告诉编译器有一个函数叫什么,参数是什么,返回类型是什么。但是具体是不是存在,无关紧要。
- 函数的声明一般出现在函数的使用之前。要满足先声明后使用。
- 函数的声明一般要放在头文件中的。
test.h放置函数声明
#ifndef __TEST_H__
#define __TEST_H__
//函数的声明
int Add(int x, int y);
#endif //__TEST_H__
(2)函数定义:交待函数的功能实现。
test.c放置函数定义
#include "test.h"
//函数Add的实现
int Add(int x, int y)
{
return x+y;
}
5、函数递归
(1)什么是递归?
把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,递归策略只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。 递归的主要思考方式在于:把大事化小
(2)递归的两个必要条件
- 存在限制条件,当满足这个限制条件的时候,递归便不再继续。
- 每次递归调用之后越来越接近这个限制条件。
例:求斐波那契数
int fib(int n)
{
if (n <= 2)
return 1;
else
return fib(n - 1) + fib(n - 2);
}
代码在运行时存在以下问题:
- 在使用 fib 这个函数的时候如果我们要计算第50个斐波那契数字的时候特别耗费时间。
- 使用 factorial 函数求10000的阶乘(不考虑结果的正确性),程序会崩溃。
在调试 factorial 函数的时候,如果你的参数比较大,那就会报错:stack overflow(栈溢出这样的信息)。 系统分配给程序的栈空间是有限的,但是如果出现了死循环,或者(死递归),这样有可能导致一直开辟栈空间,最终产生栈空间耗尽的情况,这样的现象我们称为栈溢出。
注:
1. 许多问题是以递归的形式进行解释的,这只是因为它比非递归的形式更为清晰。
2. 但是这些问题的迭代实现往往比递归实现效率更高,虽然代码的可读性稍微差些。
3. 当一个问题相当复杂,难以用迭代实现时,此时递归实现的简洁性便可以补偿它所带来的运行时开销。
四、数组
1、一维数组
(1)数组创建
type_t arr_name [const_n];
//type_t 是指数组的元素类型
//const_n 是一个常量表达式,用来指定数组的大小
注:数组创建, [] 中要给一个常量才可以,不能使用变量。数组在创建的时候如果想不指定数组的确定的大小就得初始化。数组的元素个数根据初始化的内容来确定。
(2)一维数组的使用、在内存中的存储
a、数组是使用下标来访问的,下标是从0开始。 数组的大小可以通过计算得到。
b、在内存中的存储
随着数组下标的增长,元素的地址,也在有规律的递增。 由此可以得出结论:数组在内存中是连续存放的。
2、二维数组
(1)二维数组初始化
//数组初始化
int arr[3][4] = {1,2,3,4};//其余的为0
int arr[3][4] = {{1,2},{4,5}};
int arr[][4] = {{2,3},{4,5}};//
注:
二维数组行标可以省略,列标不能省略;数组计算长度只有在定义时使用有效。
(2)二维数组在内存中的存储
二维数组在内存中也是连续存在的。二维数组是特殊的一维数组。
注:数组名代表数组首元素地址。
(两个例外,sizeof(arr)、&arr)
五、操作符
1、操作符分类
算术操作符、移位操作符、位操作符、赋值操作符、单目操作符、关系操作符、逻辑操作符、条件操作符、逗号表达式
2、移位操作符
<< 左移操作符
>> 右移操作符
(1)左移操作符
左边抛弃、右边补0
(2)右移操作符
1. 逻辑移位 左边用0填充,右边丢弃
2. 算术移位 左边用原该值的符号位填充,右边丢弃
3、位操作符
& //按位与,两位都为1时为1
| //按位或,对应位为1,结果为1
^ //按位异或,不一样的位为1,一样为0
注:他们的操作数必须是整数。
0异或任何数为数字本身;两个相同的数字异或为0。
4、逻辑操作符
逻辑操作符有哪些?
&& 逻辑与(短路与)
|| 逻辑或(短路或)
区分逻辑与和按位与 区分逻辑或和按位或
1 & 2 -----> 0
1 && 2 ----> 1
1 | 2 -----> 3
1 || 2 ----> 1
5、条件操作符
exp1 ? exp2 : exp3
//exp1为真,执行exp2,否则执行exp3
6、逗号表达式
exp1, exp2, exp3, …expN
逗号表达式,就是用逗号隔开的多个表达式。 逗号表达式,从左向右依次执行。整个表达式的结果是最后一个表达式的结果。
六、指针
1、指针是什么?
在计算机科学中,指针(Pointer)是编程语言中的一个对象,利用地址,它的值直接指向
(points to)存在电脑存储器中另一个地方的值。由于通过地址能找到所需的变量单元,可以说,地址指向该变量单元。因此,将地址形象化的称为“指针”。意思是通过它能找到以它为地址的内存单元。
总结:
- 指针就是变量,用来存放地址的变量。(存放在指针中的值都被当成地址处理)
- 指针的大小在32位平台是4个字节,在64位平台是8个字节。
2、野指针
概念: 野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)。
(1)野指针成因
- 指针未初始化
#include <stdio.h>
int main()
{
int *p;//局部变量指针未初始化,默认为随机值
*p = 20;
return 0;
}
- 指针越界访问
#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. 小心指针越界
3. 指针指向空间释放即使置NULL
4. 指针使用之前检查有效性
七、结构体
1、结构体的声明
例如描述一个学生:
typedef struct Stu
{
char name[20];//名字
int age;//年龄
char sex[5];//性别
char id[20];//学号
}Stu;//分号不能丢
结构体成员的类型:
结构的成员可以是标量、数组、指针,甚至是其他结构体。
2、结构体变量的定义和初始化
struct Point
{
int x;
int y;
}p1; //声明类型的同时定义变量p1
struct Point p2; //定义结构体变量p2
//初始化:定义变量的同时赋初值。
struct Point p3 = {x, y};
3、结构体传参
#include<stdio.h>
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函数。 原因:
函数传参的时候,参数是需要压栈的。 如果传递一个结构体对象的时候,结构体过大,参数压栈的系统开销比较大,所以会导致性能的下降。