C语言学习笔记
C语言是一门高级语言,是一门结构化语言,是面向过程的。
下面的试验均基于Dev-C++
面向过程和面向对象的区别
-
面向过程:分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候一再依次调用,面向过程以步骤划分问题
-
面向对象是把构成问题事务分解成各个对象,建立对象的目的不是为了完成一个步骤,而是为了描叙某个事物在整个解决问题的步骤中的行为。面向对象以功能划分问题
关于变量、常量要知道的
-
变量本质上是内存中的一段存储空间
-
定义一个变量,相当于请求操作系统将内存中某一块空间的使用权分配给该变量
-
变量如果不初始化使用,将会保留原来该区域中遗留的“垃圾数据”
-
常量以二进制代码存储在计算机中
- 整数以补码形式转换为二进制代码存储在计算机中
- 实数以IEEE754标准转换为二进制代码存储在计算机中,值得注意的是计算机无法精确的存储浮点数
#incude<stdio.h> int main() { float i = 99.9; printf("%f",i); return 0; }
-
字符本质上与整数的存方式相同(ASCII)
scanf和printf的基本用法
——Note:C语言中“;”作为一个语句的结束标志
#include<stdio.h>
int main()
{
int i;//定义一个变量
//printf可以直接输出字符串
printf("请输入一个数:");
//scanf是C语言中与用户交互的函数,会等待用户输入,具体用法:
scanf("%d",&i);
/*
“”号中的%d是输出控制符指以十进制类型输入
&是取地址符,意为将输入的值赋给上面定义的变量i
“”中不要加其他字符,否则输入时,要原样输入
*/
//printf也可以格式化输出
printf("这里将原样输出----%d",i);//以十进制方式输出i
return 0;
}
输入输出控制符
控制符 | 类型 |
---|---|
%d | int |
%ld | long int |
%c | char |
%f | float |
%lf | double |
%x或%X或%#x或#X | 16进制输出 |
%o | 8进制输出 |
%s | 字符串 |
test
#include<stdio.h>
int main(void){
int i = 46;
printf("%x\n",i) ;
printf("%X\n",i) ;
printf("%#x\n",i) ;
printf("%#X\n",i) ;//?
int j = 10;
printf("%o\n",j);
char str[4] = "ABC";
printf("%s\n",str);
return 0;
}
&&和|| 的短路运算
为了提升效率
-
&&左边为假,右边的表达是不会执行
-
||左边为真,右边的表达式不会执行
除法和取模运算符
-
除法/的运算结果和运算对象的类型有关,int/int的结果还是int,换句话说,小数部分被舍弃
-
要想保留小数部分,需要让除数或被除数为浮点型
-
#include<stdio.h> int main() { int i = 5; int j = 2; float m = 2.0; float residi,residf,res; residi = i/j; residf = i/m; res = i*1.0/j;//用这种方式可以简单的将转换为浮点 printf("i/j = %f\ni/m = %f\n1.0*i/j = %f",residi,residf,res); return 0; }
-
-
取余%的运算对象必须是整数,结果是整除后的余数,其余数的符号和被除数相同
-
#include<stdio.h> int main() { int i = 10; int j = -10; int m = 3; printf("i%%m = %d\nj%%m = %d",i%m,j%m);//因为%是一个特殊的字符,所以写两个%才能输出一个% }
-
流程控制
——任何复杂的算法,都可以由顺序结构、选择(分支)结构和循环结构三种基本结构组成
-
顺序:程序中的各个操作是按照它们在源代码中的排列顺序依次执行的;
-
选择:根据条件,选择一些代码执行,另外的代码不执行
#include<stdio.h>
int main(void){
float s;
printf("请输入分数:");
scanf("%f",&s);
if(s>=90)
printf("%c",'A');
else if(s >= 80)
printf("%c",'B');
else if(s >= 70)
printf("%c",'C');
else if(s >= 60)
printf("%c",'D');
else
printf("%c",'E');
return 0;
}
#include<stdio.h>
int main()
{
float s;
printf("请输入分数:");
scanf("%f",&s);
switch((int)(s/10))
{
case 10://这里没有break,就会一直执行到下一个break,这里要注意,之前有错过
case 9:
printf("%c",'A');
break;//出口
case 8:
printf("%c",'B');
break;//出口
case 7:
printf("%c",'C');
break;//出口
case 6:
printf("%c",'D');
break;//出口
default:
printf("%c",'E');
break;//出口
}
return 0;
}
/*
switch语句规则:
1、switch语句非常有用,但在使用时必须谨慎。所写的任何switch语句都必须遵循以下规则:
2、只能针对基本数据类型中的整型类型使用switch,这些类型包括int、char等。对于其他类型,则必须使用if语句。
3、switch()的参数类型不能为实型 。
4、case标签必须是常量表达式(constantExpression),如42或者'4'。
5、case标签必须是唯一性的表达式;也就是说,不允许两个case具有相同的值。
*/
循环
-
for循环
-
单层循环:求1-100的和
#include<stdio.h> int main() { int sum = 0; for(int i = 1;i<=100;i++) { sum += i; } printf("%d",sum); return 0; } /* result:5050 */
-
嵌套循环:输出:
* *** *****
#include<stdio.h> int main() { for(int i = 0;i < 3;i++) { if(i!=0) { printf("\n"); } for(int k = 0;k<3-1-i;k++) { printf(" "); } for(int j = 0;j<2*(i+1)-1;j++) { printf("*"); } } return 0; }
-
-
while循环
-
while循环和for循环可以互相转换
1; while(2) { A; 3; } for(1;2;3) { A; } /* 1一般只执行一次 2成立执行A A执行后执行3 3成执行后执行2 2不成立循环结束 */
-
-
do while循环
/*
do while 和for、while不一样,他至少执行一次
*/
do{
...
}while(表达式)
break和continue
-
break如果用于循环是用来终止循环,在多层循环中, break只能终止最里面包裹它的那个循环
-
break如果用于 switch,则是用于终止 switch
-
break不能直接用于if,除非if属于循环内部的一个子句
-
continue用于跳过本次循环余下的语句,转去判断是否需要执行下次循环
while((ch = getchar())!='\n') continue; /* 该语句为了屏蔽用户的某些非法输入 比如在前面输入了111abd 在执行了111还有残留的abd,可能对下一次的输入产生影响,该语句可屏蔽该影响 */
静态数组和动态数组
——为了解决大量同类型数据的存储和使用问题
-
静态数组
/* 一维数组 为n个变量连续分配存储空间 所有变量的数据类型必须相同 所有变量所占字节大小必须相同 数组名表示数组第一个元素的地址 构建静态数组时,[]中不能写变量,也就是说不能通过用户输入的值来确定数组长度 */ int a[5];//声名数组,不初始化,所有元素都是垃圾值 int a[5] = {1,2,3}//不完全初始化,其他变量是0 int a[5] = {1,2,3,4,5};//声名并完全初始化 int a[5] = {0};//清零 /* 二维数组 int a[3][4];//可以看作3行4列 */ /* 多维数组 计算机的内存是线性一维的 严格意义上不存在多维数组 n维数组可以当做每个元素是n-1维数组的一维数组 */
-
动态数组
#include<stdio.h> #include<malloc.h> int main() { int len; int * pArr; int i; //静态构造一维数组 int a[20];//如果int占4个字节,则本数组共包含有20个字节,每四个字节当作一个int变量使用 //动态的构造一维数组 printf("请输入要存放元素的个数:"); scanf("%d",&len); pArr = (int *)malloc( sizeof(int) * len); //等同 int pArr[len] 本行 动态的构造了一个一维数组 for(i = 0;i<len;i++) { printf("请输入第%d个数:",i+1); scanf("%d",&pArr[i]); } for(i = 0;i<len;i++) { printf("%d ",pArr[i]); } free(pArr);//释放动态数组 return 0; } /* pArr指向动态数组的第一个元素的地址 *pArr 等同于pArr[0] 因为连续 所以*(pArr+i) 等价于 pArr[i] 动态分配的一维数组使用方式与静态几乎类似 */
函数
——为了避免重复性的操作,有利于程序的模块化
/*
函数的使用,输出1-100内的素数
*/
#include<stdio.h>
#include<math.h>
//定义判断素数的函数
bool JudgePrime(int num)
{
for(int i = 2;i<=sqrt(num);i++)
{
if(num%i==0)
{
return 0;
}
}
return 1;
}
//给定一个整数,输出1-该整数中所有的素数
void Traverse(int N)
{
for(int i = 2;i<N;i++)//1不是素数
{
if(JudgePrime(i))
{
printf("%d ",i);
}
}
}
int main()
{
/*
将步骤写成函数,可以让主函数变得更加简洁,同时减少了不必要的重复性操作
*/
int N;
printf("请输入数字N,我来帮你求1-N的所有素数:");
scanf("%d",&N);
Traverse(N);
return 0;
}
//注意:如果函数调用写在了函数定义的前面,则必须加函数前置声明
指针
——指针可以用于表示一些复杂的数据结构(树、图)
——快速的传递数据,减少内存的耗用
——使函数放回一个以上的值
——能直接访问硬件
——能够方便的处理字符串(\0休止符的存在)
——理解面向对象语言中引用的基础
-
指针就是地址,所谓地址是内存单元的编号,范围[0,内存大小(比如8G)-1]
-
指针变量和指针不一样,指针变量是存放指针(地址)的变量,指针变量的占字节的大小与类型无关,与操作系统位数和编译器环境相关,32位4个字节,64位8个字节。
-
#include<stdio.h> int main() { int * p; double * k; printf("int * 占的字节数:%d\ndouble * 占的字节数:%d",sizeof(p),sizeof(k)); //sizeof函数用于返回变量占的字节数 return 0; }
-
在64位系统,64位编译环境下:
-
在64位系统,32位编译环境下:
-
-
-
指针的本质是一个操作受限的非负整数
基本类型指针
-
基本用法和概念辨析
-
#include<stdio.h> #include<math.h> int main(void) { int * p;// 定义指针变量p,指针变量p存放的是----int类型----的----地址---现在p放的是垃圾值 int i = 5;//将5赋给系统分配的名称为i的内存空间 p = &i; //这里是第8行,将i的地址赋给指针变量i &是取地址符,能够获得变量的内存单元的编号 *p = i; /* 若第8行被注释掉,这里的写法是危险的,因为将i赋给了未经系统分配的内存空间 若第8行没被注释,这里指将i赋给以p的内容为地址的变量即i */ /* p保存了i的地址,所以p指向i p和i不同,p相当于复制的i的地址,修改p的值不影响i的值,修改i的值不影响p的值 如果一个指针变量指向一个普通变量 则 *指针变量 与该普通变量等价 eg: 如果p是个指针变量,并且p保存了普通变量i的地址,则 p指向了普通变量i *p 完全等同于 i 或者说: 在所有出现*p的地方可以替换成i 同理 在所有出现i的地方可以替换成*p *p中的*相当于取地址符的逆操作,换句话说就是以p的内容为地址的变量 指针VS指针变量 指针:地址【内存单元中具体的(编号)值】 指针变量 :存放指针的变量 */ printf("%d",*p);//若第八行没有被注释,这里将输出5 return 0; }
-
-
通过被调函数修改主调函数的值
-
#include<stdio.h> //通过获取主调函数中变量的地址,来改变主调函数中该变量的值 void changeIOut(int * p)//形参为指针变量,接受主调函数中传来的变量的地址 { *p = 100;//通过* ,将100赋给以p的内容(&I)为地址的变量(I) } int main() { int I = 10; printf("I before:%d\n",I); changeIOut(&I); printf("I after:%d",I); return 0; }
-
指针和数组
-
指针和一维数组
- 一维数组名是个指针变量
- 它存放的是一维数组第一个元素的地址
-
数组下标和指针的关系
-
如果p为指针变量,则p[i]等价于*(p+i)
-
确定一个一维数组需要两个参数
- 数组第一个元素的地址
- 数组的长度
#include<stdio.h> int main() { int a[5] = {1,2,3,4,5};//定义一个数组,并初始化 int * p = a;//将a赋给p,如果a是地址的话,这里才不会报错,相同类型的变量才能互相赋值 //如果,上面没有问题,这里验证*(p+i)与a[i]是否等价 for(int i = 0;i<5;i++) { printf("a[%d]:%d\n",i,a[i]); printf("*(p+%d):%d\n",i,*(p+i)); printf("-----------\n");//无意义,为了看的清楚点 } return 0; }
变量的地址
- 一个变量的地址,是用该变量的首地址来表示的,那么如何保证变量的完整?
- 变量类型划分了从首地址开始,以该变量类型所占字节为长度的变量,这就是为什么定义变量时一定要给出类型的原因
指针与函数
#include<stdio.h>
/*
以下函数不能完成互换功能
R:虽然都叫变量a,和变量b
但是函数里的a,b与主函数的不同
形参和实参是不一样的!!!!!!!
*/
void SwitchAB_1(int a,int b)
{
int t;
t = a;
a = b;
b = t;
return;
}
/*
以下函数不能完成互换功能
只是互换p,q的内容,对a,b没有影响
要根据p,q的 内容,找到它们对对应的变量,并互换变量内的值
*/
void SwitchAB_2(int * p,int * q)
{
int * t;
t = p;
p = q;
q = t;
return;
}
//可以完成互换
void SwitchAB_3(int * p,int * q)
{
int t;
t = *p;
*p = *q;
*q = t;
return;
}
//可以完成互换
void SwitchAB_4(int &a,int &b)
{
int t;
t = a;
a = b;
b = t;
return;
}
int main(void)
{
int a=5,b=6;
int t;
//下面3行是主调函数内完成的互换
t=a;
a=b;
b=t;
printf("正常的互换:a:%d b:%d\n",a,b);//a:6 b:5
SwitchAB_1(a,b);
printf("later1 a:%d b:%d\n",a,b);//a:6 b:5 失败
SwitchAB_2(&a,&b);//SwitchAB_2(a,b)是错误的
printf("later2 a:%d b:%d\n",a,b);//a:6 b:5 失败
SwitchAB_3(&a,&b);
printf("later3 a:%d b:%d\n",a,b);//a:5 b:6 成功
SwitchAB_4(a,b);
printf("later4 a:%d b:%d\n",a,b);//a:6 b:5 成功
return 0;
}
int test(int * p, int * q){
int t;
t = *p;
*p = *q;
*q = t;
return;
}
指针变量的运算
-
指针变量的加法,乘法,除法没有意义
-
如果两个指针变量指向的是同一块连续空间中的不同存储单元,那么它们相减意味着隔了”多远“
-
注意:
- 指 针 相 减 = ( 地 址 1 − 地 址 2 ) / s i z e o f ( 类 型 ) 指针相减=(地址1-地址2)/sizeof(类型) 指针相减=(地址1−地址2)/sizeof(类型)
多级指针
#include<stdio.h>
int main()
{
int i = 6;
int * p = &i;//p只能存放int类型变量的地址
int ** q = &p;//q只能存放int * 类型变量的地址 那么*q == p **q == i 地址的地址
int *** r = &q;//r只能存放int ** 类型变量的地址 地址的地址的地址 //禁止套娃 ε(┬┬﹏┬┬)3
printf("***r:%d **q:%d *p:%d",***r , **q, *p);
return 0;
}
静态和动态分配内存
-
静态数组的缺点
- 长度必须事先制定,只能是产量不能为变量
- 静态数组的内存不能由程序员手动释放,必须要等包含该数组的函数运行完毕,系统才会自动释放
- 数组的长度不能在函数运行过程中扩充或缩减
- 由于静态变量、数组存在在栈中,也就意味着,不可能跨函数调用
-
动态分配内存为了解决上述缺点
-
malloc函数的用法
#include<stdio.h> #include<malloc.h> /* malloc (memory allocate 内存分配)函数的用法 int * p = (int *)malloc(int len); //事实上 ,int len为保证安全,更常写为:sizeof(int)*len malloc 函数的功能是请求系统分配len个字节的内存空间, 成功则返回第一个字节的地址,失败则返回NULL 由于为了保证变量的完整性,需要强制类型转换,前面加(int *),以具体划分 比如 double * p = (double *)malloc(80); 这里向系统请求分配80个字节的内存空间,并将第一个字节的地址返回给p, 同时又指定了double * 类型,系统就知道从第一个地址开始后续的8个字节作为一个整体 即p 指向了第一个 8个字节,p+1指向了第二个 8个字节... */ int main(){ int 5;//静态分配了5个字节 int * p = (int *)malloc(4);//这里共分配了8(p本身 64位环境)+4 = 12个字节 /* 1.要使用malloc函数,必须添加malloc.h头文件 2.malloc只有一个形参,并且形参为整形 3.4表示请求系统为该函数分配4个字节 4.malloc函数只能返回第一个字节得地址 5.所以要通过指定类型来指定第一个字节得地址是什么类型的地址 6.p本身所占得内存是静态分配得,而p指向的内存是动态分配的 */ *p = 5; //动态分配的可以手动释放 free(p);//表示把p指向的内存给释放掉 p本身的内存是静态的,不能由程序员释放,只能在函数执行完后,自动释放 return 0; }
-
-
动态内存和静态内存的对比
-
静态变量不能跨函数使用内存
#include<stdio.h> void f(int ** q)//q是个指针变量,无论是 什么类型的指针变量,都只占8个字节(64 位环境) { int i = 5;//静态变量不能跨函数使用,因为它们存储在栈中,出栈意味着,资源被释放,也就没有读写权限 //*q等价于p *q = &i;//p = &i; } int main() { int * p; f(&p); printf("%d\n",*p);//这里逻辑上存在问题,i在f函数执行完后就已经释放了,*p访问了一个没有权限的变量 return 0; }
-
动态变量跨函数使用内存
#include<stdio.h> #include<malloc.h> void f(int ** q) { *q = (int *)malloc(sizeof(int)*1);//动态分配的变量在堆内,函数执行后,不会被释放 **q = 5; } int main() { int * p; f(&p); printf("%d\n",*p); //输出为5 return 0; }
-
总结
- 静态内存是由系统自动分配,由系统自动释放,是在栈分配的
- 动态内存是由程序员手动分配,可手动释放,是在堆分配的
- 静态内存不能跨函数使用,是因为,静态内存在栈中分配,一旦该函数出栈,意味着函数中的静态内存将被释放,该内存空间的权限被收回,在另一个函数中调用系统未分配的空间是危险的
- 动态内存则可以跨函数使用,因为它分配在堆中,即便函数出栈,也不影响它。
-
结构体
——为了表示基本数据类型无法表示的事物,比如学生,他可以有姓名,性别,分数等,需要多个变量综合起来才能描述的事物
——结构体,是一些基本类型数据组合起来形成的新的复合数据类型
-
结构体定义,基本用法
#include<stdio.h> //define 结构体的第一种方式 推荐 仅仅定义了一个新的数据类型,并没有实例化 struct Student { int age; float score; char sex; }; //define 2 struct Student2 { int age; float score; char sex; }st2 ; //define 3 struct { int age; float score; char sex; }st3; int main() { struct Student st1 = {80,66.6,'F'};//结构体的赋值和初始化(定义的同时赋初值) struct Student st2 ; //结构体访问成员变量 1 结构体变量名.成员名 st2.age = 66; st2.score= 90; st2.sex = 'M'; printf("%d %f %c\n",st1.age,st1.score,st1.sex); printf("%d %f %c\n",st2.age,st2.score,st2.sex); //结构体访问 2 指针变量名->成员名 用的多 struct Student * pst = &st1;//因为st1 是Student类型则,指向它的指针也是该类型 pst->score = 66.7f;//pst->age在计算机内部会被转换成(*pst).age pst->age = st.age 66.7在C语言中默认是double类型 printf("%f ",st1.score) ; //pst->age 含义:pst所指向的那个结构体变量中的age成员 return 0; }
-
一个例子(综合性)
#include<stdio.h> #include<malloc.h> //定义Student结构体,由age,score,name 属性 struct Student { int age; float score; char name[100]; }; //输入学生信息 void InStInfo(struct Student * p,int i) { printf("请输入第%d个学生的信息:\n",i+1); printf("age : ") ; scanf("%d",&p[i].age); printf("name : ") ; scanf("%s",p[i].name);//name 本身是数组名,所以在scanf 函数里不需要加取地址符 printf("score : ") ; scanf("%f",&p[i].score); } //输出学生信息 void OutStInfo(struct Student * p,int i) { printf("第%d个学生的信息:\n",i+1); printf("age : %d\n",p[i].age) ; printf("name : %s\n",p[i].name) ; printf("score : %f\n",p[i].score) ; printf("\n"); } //通过冒泡排序按学生升序排 void sortUp(struct Student * p,int * len) { for(int i = 0;i<*len;i++) { for(int j = 0;j<*len-1-i;j++) { if(p[j].score>p[j+1].score)//根据学生成绩高低,给学生整体排序,而不是单纯的换成绩本身,那没有意义 { struct Student t;//换的是学生,所以变量类型是struct Student类型 t = p[j]; p[j] = p[j+1]; p[j+1] = t; } } } } int main() { int len; struct Student * pArr; printf("请输入学生的个数:\n"); printf("len = "); scanf("%d",&len); pArr = (Student *)malloc( len * sizeof(Student) ); for(int i = 0;i < len;i++) { InStInfo(pArr,i); } //排序 sortUp(pArr,&len); //输出 printf("\n\n学生的信息如下:\n\n"); for(int i = 0;i<len;i++) { OutStInfo(pArr,i); } return 0; }
枚举
——把所有事物的取值意一一列举出来