C_数据结构与算法(一):C语言基础 - Rudolph_Browne
c_数据结构与算法(一):c语言基础
致初学者的我:一切都是由浅入深。
每种语言都有每种语言的特性,基本的特性是相同的,下面依照惯例写hello world,相关编译后面再介绍。
// c语言用“//”和“/**/”来写注释,注释、空格等不影响语义(程序的意思)的符号会在程序编译阶段会被删除。 #include //#include 预编译处理,这里把stdio的c函数库载入进程序; main() //main() 主程序入口; { printf("hello world!"); //printf() stdio函数库中的函数,表示格式化输出; return 0; //return 0 返回状态0,表示程序正常结束。 }
part1 基本数据类型及表达式
变量可以视为一个无限大的黑箱,你可以往里面装你想装的东西,三个要素:
定义(或声明),根据数据类型在内存中申请空间,如int age;
赋值:修改变量的值,如 age = 30;
使用:将变量的值从内存中读出来进行使用,如 printf("%d",age),或运算 age = age + 1;
int age = 23 数据类型 变量名 赋予变量的值
c基本数据类型(计算机中位指的是'0'或‘1’其中之一,而八位称为一个字节,sizeof()函数可以查看数据类型具有多少个字节):
- 整数型
short n = 10; //内存中占16位 int n = 10; //占32位 long n = 10; //占64位
float f = 1.1; //占32位,有效位数6~7 double d = 1.1; //占64位,有效位数15~16
char c = 'a'; //占8位
- 理解进制(以十进制138为例,可以用程序员计算器来帮助日常换算)
二进制: 138 = 128(即1*2^7) + 8(1*2^3) + 2(1*2^1) ----->10001010 八进制: 138 = 128(2*8^2) + 1(1*8^1) + 2(2*8^0) ----->212 十进制: 138 = 100(1*10^10) +30(3*10^1)+ 8(8*10^0) ----->138 十六进制: 138 = 128(8*16^1) + 10(10*16^0) ----->8a //十六进制中 10-15 分别以a-f来表示
- (unsigned ) char、 short、int、long、float、double等类型之间可以互相赋值,会进行自动类型转换:
short s = -1; unsigned short us = s; // us = 65535 short s2 = 65; char c = s2; //c = 'a'
精度损失:如果把存储大的类型赋值给小的类型,会面临精度丢失的问题。
变量相关信息 命名规则:变量的命名规则可以让程序更容易理解,个人采用camel-case命名法,如:personalinformation 作用域:指包括此变量声明的最小代码块,在这个作用域中,变量声明后的所有代码行都可以访问此变量。变量在其作用域内被创建,离开其作用域时被撤销。{ int age = 10; ... ....以上这些行都可以访问age } 这里不能访问age
- 一般我们在代码中通过符号来代表常量,这个符号被称为符号常量,通过#define或const定义,例如:
const double pi = 3.14; //定义圆周率的符号常量 #define pi 3.14; //用宏定义的方式,分不出数据类型 printf("pi=%f\n",pi); //注:尽量使用const来定义符号常量
注意:尽量避免使用魔法数,指的是在程序中多处出现的常量值,应该使用上述方法将其赋值给一个恒定的变量。
表达式 - 表达式由操作符和操作数构成,通过操作符将一个或多个操作数连接到一起,如:
id = 3 //赋值表达式, =是赋值操作符 1+2 //计算表达式,+是算术运算符 ++i //计算表达式,++是算术计算符 2>1 //逻辑表达式,>是关系运算符符 (double)a //类型转换表达式 1 && 0 //逻辑表达式,&&是逻辑运算符 ...
- ++、–表示对变量执行加1(减1)操作,同时参与表达式运算。例如
int a = i++ + 3; //先让i参与表达式运算,然后将i+1 int a = ++i + 3; //先将i+1,然后让i参与运算
- &:位与运算,将二进制每一位进行与运算,都是1则为1,如
3 & 5 = 1 0011 & 0101 = 0001
|:位或运算,将二进制每一位进行或运算,都是0则为0,如3 | 5 = 7 0011 | 0101 = 0111
^:位异或运算,将二进制每一位进行异或运算,不相同为1,相同为0,如3 ^ 5 = 6 0011 ^ 0101 = 0110
~:按位取反,如~3 = -4
<<:左位移,将左端挤掉,右端补零,如3<<1 = 6 011 --> 0110
>>:右位移,对有符号数,最左边补原符号数,其它左边位补0,右边被挤掉,如3>>1 = 1 011 -->01
赋值表达式 - 左值与右值:对一个变量(或表达式)来说,有两个值与其关联:
char[] fullname = "rudolph_browne";//fullname为左值,rudolph_browne为右值 1)数据:变量的数据值,存储在地址中,这个值被称之为右值(rvalue),r是read(可读)的意思。 2)地址:存储数据值的内存地址,地址被称之为左值(lvalue),l是location(可寻址)的意思。
“每一个表达式要么是lvalue,要么是rvalue”。左值指表达式结束之后依然存在的持久对象,而右值指表达式结束之后就不复存在的(临时)对象。
其他表达式 - sizeof运算符:返回变量或类型的字节数,如
sizeof(float); //4 sizeof(double); //8 int n; sizeof(n); //4
int a = 1?5:10; //a为5 int b = (a%2==0)?1:0; //a为偶数时,b=1,否则为0
a=3, b=a+1, c=b+1; x = (a=3, b=6*3); //逗号表达式的值为最后一个表达式的值 (a=3,b) = 5; //最后表达式为左值时,逗号表达式也可为左值
part2 程序结构
- printf函数概述
printf("格式控制字符串"[,输出列表]); 注意:输出列表与格式控制字符串之间有一个逗号分隔(如果输出列表存在的话)。
printf("%05hd\n",10); //h表示短类型,l表示长类型 输出短整型数据10,宽度为5位,前面补0 //00010
d(i):十进制整数,负数带-号 u:无符号十进制整数 o:无符号八进制整数,无前导字母o x(x):无符号十六进制整数,无前导0x,x表示大写 f:小数形式的浮点数,默认6位小数 e(e):指数形式的浮点数 g(g):自动选择e和f中宽度较小者,不打印无效的0 c:单个字符 s:字符串
putchar(c); //c可以是字符或整型数字
scanf("格式控制字符串",地址列表); scanf("%d",&a); //&a表示变量a的地址
注意:通常在用c库函数输出提示语句时都会适当地缓存,要输出缓存时使用下面语句。
setbuf(stdout,null); //这句代码是为了先输出提示语句
char c = getchar(); //等待键盘输入一个字符给变量c
线性结构 - 线性结构的程序没有太多的特殊性,程序按定流水线的方式被执行。 选择结构
- if-else if-else结构
if(条件1) //如果条件为真 { //那么 //当条件成立时执行这里的代码 }else if(条件2) { //或者 //当条件2成立时执行这里的代码 }else { //否则 //执行这里的代码 }
switch(rank){ case 1: //奖励iphone; break; case 2: //奖励ipad; break; case 3: //奖励ipod break; default: //无视 }
- while语句不断判断循环条件,当循环条件为真(非0)的时候,就执行循环操作,一直到循环条件变为假(0)才跳出。
while(循环条件){ //循环操作 }
do{ //执行循环操作 }while(循环条件); //注意有分号
注意:while和do-while是在程序不知道具体停止位置而使用的,如果知道具体位置,请使用for。
for循环一般用于循环次数确定的情况,它将条件的初始化、条件判断、条件变化抽取出来,和循环体分离。for(参数初始化;条件判断;参数更新){ //循环操作 }
while(...){ .... break; //下面的语句执行不到了,因为break跳出了循环 printf(...); } //break以后,执行这个语句
for(int i=0; i< 100; ++i){ if(如果是奇数){ continue; } sum += i; }
#include int main(){ int i,j; for(i=0;i<4;++i){ printf("%d\n",i); for(j=0;j<4;++j){ if(i*j==4){ goto l1; } printf("\t%d:\n",j); } } l1: printf("循环跳出,i=%d,j=%d\n",i,j); return 0; } //注:在c语言中,由于goto的灵活性,一般不提倡使用goto进行跳转。
part3高级数据类型
- 数组是一个变量,用来存储相同类型的一组数据。数组四要素:变量名称、元素、下标(从0开始)、类型,通过下标访问数组的元素。
类型 变量名称 int size = 3; int narray[3] = {1,2,4}; int d1 = narray[0];//0是下标,narray[0]是下标为0的元素 //注意下标不能超出数组的长度-1 for(i=0;i <= size;i++){ //循环体 }
int a[4]; //a[0](假设地址为2000h)、a[1]、a[2]、a[3] 2000h 2004h 2008h 200ch a[0] a[1] a[2] a[3]
int a[4]={1,2,3,4,5}; //编译提示越界
但在运行时访问下标的时候,编译器不会检查越界,此时程序员要自己注意。例如:
int a[4]={1,2,3,4}; printf("%d",a[4]); //a[4]指向2010h地址,内存的值未知 a[4] = 100; //如果修改了a[4]的值,实际是修改了别人的数据,可能导致程序出错或崩溃
1)int a[4]={}; 2) #include int a[4]; memset(a, 0, 4*sizeof(int)); //以字节数为单位赋值 3)for循环将每个元素设置成0
注:static变量或全局变量自动会初始化成0。
数组地址分配的规则,用&显示变量的地址,注意结果地址的增加字节数。#include int main(){ int i,a[4]; for(i=0;i<4;++i){ printf("a[%d]的地址是:%p\n",i,&a[i]); } printf("数组变量a的地址是:%p\n",&a); return 0; }
输出结果为:
=====运行开始====== a[0]的地址是:0x7fff1d9e5ae0 a[1]的地址是:0x7fff1d9e5ae4 a[2]的地址是:0x7fff1d9e5ae8 a[3]的地址是:0x7fff1d9e5aec 数组变量a的地址是:0x7fff1d9e5ae0 =====运行结束======
int i, max=arr[0]; for(i=1;i
int i,sum=0; for(i=0;i
int value = 5; //要查找的数 int i; for(i=0;i
for(i=3; i
删除给定值的元素:先通过查找获得下标,然后执行1。
int find=0; int removeindex; for(removeindex=0; removeindex
int a[6] = {1,2,3,4,5}; //注意数组长度要够 for(i=len-1; i>2; --i){ //注意i从大到小 a[i] = a[i-1]; //依次往后移动,空出a[2] } a[2] = 10;
for(i=0; i
合并:同样可以用循环赋值或memcpy函数。
int a[3]={1,2,3}, b[4]={4,5,6,7}; int c[7]; for(i=0; i<7; ++i){ c[i] = (i<3)?a[i]:b[i-3]; } //或:memcpy(c, a, 3*4); //memcpy(c+3, b, 4*4);
int scores[2][4] = {{1,2},{2,3},{3,4,5,6}}; for(int i=0; i<2; i++){ for(int j=0; j<4; j++){ printf("%d",scores[i][j]); } printf("\n"); }
结果为
=====运行开始====== 1200 2300 //因为2行中赋值中有{1,2,(0,0)}和{2,3,(0,0)},即不足的以0来填补。 =====运行结束======
scores[3][5],三个行如下: scores[0] --> 一班的分数:5个元素的一维数组 scores[1] --> 二班的分数:5个元素的一维数组 scores[2] --> 三班的分数:5个元素的一维数组 共15个元素,按照顺序存储,如: 2000h 2001h 2002h 2003h 2004h 00 01 02 03 04 2005h 2006h 2007h 2008h 2009h 10 11 12 13 14 2010h 2011h 2012h 2013h 2014h 20 21 22 23 24
矩阵算法可以使用二维数组进行运算,如a[3][3]:
1 2 3 --> a[0] 4 5 6 --> a[1] 7 8 9 --> a[2]
字符串 - 概念:在c语言中,将字符串作为字符数组处理,以’\0’作为字符串的结束标志,’\0’占内存空间,但不计入字符串长度。
"hello" ---> {'h','e','l','l','o','\0'} //字符串长度为5,数组长度为6
输出字符串数组变量时,遇到’\0’结束,’0’是ascii码的0,可以通过循环输出看到,例如
char a[] = "hello"; int len = sizeof(a)/sizeof(a[0]); //6 printf("%s\n",a); //hello for(int i=0; i 最后一个是 0
字符数组的定义和赋值:
char a[] = "hello"; char a[] = {"hello"}; char a[] = {'h','e','l','l','o','\0'}; char a[]; a = "hello"; //错误,定义后就是常量,不能再整句赋值
拷贝函数strcpy:strcpy(目标字符数组, 源字符数组),将源字符数组的内容(包括结束符\0)拷贝到目标字符数组中,函数返回目标字符数组。 char s[10] strcpy(s, "hello"); //s-->hello\0
strcpy的覆盖性:如果目标字符数组中已经存在内容,将被覆盖。
char s[10]="123456789" strcpy(s, "hello"); //s-->hello\0789 printf("%s",s) //hello
strncpy:strncpy(目标字符数组,源字符数组,拷贝长度),将源字符数组的前n个字符(不一定包括结束符\0)拷贝到目标字符数组中,函数返回目标字符数组。
char s[10]="123456789" strncpy(s, "hello",3); //s-->hel456789 printf("%s",s); //hel456789
strcpy的溢出性:如果目标字符数组的长度比源字符数组的长度短,strcpy函数会导致溢出,即目标字符数组地址后面的内存地址会被写入多出的字符,产生不可预期的后果。
char a = 'a'; char s[3]; strcpy(s,"hello"); //s-->hello\0 --> 可能会导致a被修改
char s1[20] = "i am"; char s2[] = "a boy"; strcat(s1,s2); //s1-->i am a boy\0 printf("%s",s1); //i am a boy
字符数组1 = 字符数组2, 返回值=0 字符数组1 > 字符数组2, 返回值>0 字符数组1 < 字符数组2, 返回值<0
当需要判断两个字符串是否相等,例如判断密码是否为“137000”,代码如下:
char pwd[] = "137000"; int b = strcmp(pwd,"137000"); //b == 0
char s[20] = "hello"; printf("%d",strlen(s)); //5
strlen与sizeof的区别
char s[10] = "hello"; printf("%d",strlen(s)); //5 printf("%d",sizeof(s)); //10
1)sizeof是编译时计算,关注分配了多少空间,不关注实际存储的数据 2)strlen是运行时计算,关注存储的数据内容,不关注分配的空间。
函数 - 概念:函数是程序的一个包含单元,用于完成某一特定的任务,比如一类计算、一种操作等等,使用函数对这些任务进行封装,减少重复编码,使程序更加模块化,增强可读性和可维护性。例如
int main(){ //打印三个*行 int i; for(i=0; i<3; ++i) printstar(); }
函数的调用:函数是一个黑匣子,外部无需知道函数里面具体怎么执行的,只需要调用函数,获取返回的结果即可。
函数定义的语法:返回值类型、函数名、形参列表、函数体,例如返回值类型 函数名(参数列表) void printstar(int n, char c){ //在这里输出*号组成的行 }
函数需要先定义、后调用,即函数的定义应当在主调函数之前,如
void printstar(int n, char c){} int main(){ printstar(5,'*'); ... }
有时候我们想先调用函数,之后再定义函数,则需要在调用之前先声明,这和变量的声明、使用、赋值类似,如
int main(){ void printstar(int n, char c); //声明,注意没有函数体 printstar(5,'*'); } void printstar(int n, char c){//定义这里有函数体}
调用系统库函数,要使用#include<库文件>,在中,库文件使用*.h文件,例如
#include #include //使用库文件 int main(){ printf("%d的绝对值是%d", -3,abs(-3)); //输出绝对值 return 0; }
#include int add(int a, int b){ printf("\t开始执行add函数,参数%d,%d\n",a,b); int sum = a + b; printf("\tadd执行完成,准备返回%d\n",sum); return sum; } int main(){ printf("开始执行main函数\n"); printf("调用add函数\n"); int a = add(3,5); printf("调用add函数返回:%d\n",a); a = add(1+2,2*4); printf("调用add函数返回:%d\n",a); printf("main函数执行结束\n"); return 0; }
=====运行开始====== 开始执行main函数 调用add函数 开始执行add函数,参数3,5 add执行完成,准备返回8 调用add函数返回:8 开始执行add函数,参数3,8 add执行完成,准备返回11 调用add函数返回:11 main函数执行结束 =====运行结束======
int add(int a, int b){ //在这里可以使用变量a和b }
参数列表的定义相当于定义了局部变量,其作用域是函数体,在函数没有被调用时,形参没有内存分配,当函数调用时被分配内存,函数结束后,内存被释放。
实参:在函数调用时,传入与形参类型一致、个数一致的参数列表,如果不相同会执行自动格式转换。调用时不需要在前面加变量类型。int main(){ int sum = add(3.2, 5); //3.2传给a(自动转换),5传给b }
int change(int a){ return ++a; } int main(){ int a = 5; change(a); printf("%d",a); //a还是5,不会变成6 }
1)静态存储区:主要存放全局变量、静态变量(static)、字面常量,其生存期是从程序开始到程序结束; 2)动态存储区:主要存放局部变量、参数等,在程序运行期间动态分配,其中栈和堆的使用又有所不同。
{ int a; ---> auto int a; }
自动变量属于动态存储方式,当函数被调用时才分配存储,函数结束即自动释放。作用域是定义它的单元,如函数或代码块。
静态变量:说明符为static,在函数中的static变量属于静态存储方式,程序启动时被分配,程序结束后释放。作用域取决于其定义的位置,函数内定义的作用域就是函数体,函数外定义的作用域就是本文件。例如{ static int a; //函数结束不会释放,下次还可以继续用 }
static的理解:具备静态存储特性,又具备函数的作用域。
注:全局变量或函数前面加static,表示作用域在本文件内部,不能被其它文件引用。
外部变量:说明符为extern,全局变量的默认方式,当文件中要引用另一文件中的全局变量,或者在全局变量定义之前要引用它时,可以用extern做说明。例如file1.cpp file2.cpp int global; <--- extern int global; //用到file1.cpp
int main(){ register int i, sum=0; }
int sum(int a, int b){return a+b;} int avg(int a, in b){return sum(a,b)/2;}
int sum(int a){return a+sum(a+1);}
递归的2个条件: 1)有反复执行的过程(自己调自己) 2)有跳出反复过程的条件(递归出口)
//由于传递的是地址,不需要指定个数,需要传入个数的参数 void changearr(int a[], int len){ a[0] = 100; } int main(){ int a[5] = {1,2,3,4,5}; changearr(a,5); //传入的是数组名 printf("%d",a[0]); }
int max(int a[], int len){ int i, max = a[0]; for(i=0; i
struct student //student是新定义的结构名 { int no; //学号 char name[10]; //姓名 int age; //年龄 char address[30]; //地址 }; //注意分号
结构体体现了数据的封装性,用结构体变量进行信息传递,一方面可以减少参数的数量,另一方面当业务逻辑有变化时,可以很方便的修改结构体的定义,不需要修改传递接口(函数定义)。例如:
struct car { ... } //当汽车增加新成员字段时,不需要修改buycar的函数定义 void buycar(struct car c){ .... }
结构体变量作为函数参数时,采取的是值传递的方式,即形参产生了和实参同样数据的一个内存拷贝。一方面,结构体数据多的时候内存开销大,另一方面,函数里面对数据的修改不会影响到实参。如
void changecar(struct car c){ c.price = 200; } struct car c = {"宝马","x5","3.0","红色",130}; changecar(c); //在函数里修改价格 printf("%d",c.price); //价格还是130
如果想让结构体变量在函数中被修改,跟其他类型一样,可以使用指针,如
void change2(struct car *c){ c->price = 200; //指针引用成员用 -> 运算符 } change2(&c);
结构体作为返回值:当函数需要返回多个值时,可以用结构体变量。例如
struct point { int x; int y; }; struct point createpoint(){ struct point p = {0,1}; return p; }
union test { int i; char c; float f; } a, b, c;
注意:一个联合体变量一次只能给一个成员赋值,后赋值会覆盖前面的赋值,只有最后赋值的成员才有意义。
#include union test{ int i; char c; float f; }; int main(){ union test t ; printf("i=%d,c=%d,f=%f\n",t.i,t.c,t.f); t.i = 10; printf("i=%d,c=%d,f=%f\n",t.i,t.c,t.f); t.c = 'a'; printf("i=%d,c=%d,f=%f\n",t.i,t.c,t.f); t.f = 1.0f; printf("i=%d,c=%d,f=%f\n",t.i,t.c,t.f); return 0; }
=====运行开始====== i=1314011120,c=-16,f=881720320.000000 i=10,c=10,f=0.000000 i=97,c=97,f=0.000000 i=1065353216,c=0,f=1.000000 =====运行结束======
enum weekday {sun,mon,tue,wed,thu,fri,sat}; enum weekday day1 = sun; //变量的值只能是枚举元素之一
#include enum weekday {sun,mon,tue, wed,thu,fri,sat}; int main(){ enum weekday day1 = sun, day2; printf("day1 = %d\n",day1); day2 = (enum weekday)(day1 + 3); printf("day2 = %d\n",day2); if(day2 == wed) printf("星期天过3天是星期三\n"); return 0; }
typedef struct student{ int no; string name; } student; //定义新的结构体类型student student zhangsan;
指针 概述 变量的内存结构:类型、变量名、地址、数据:int n = 10; 2001h~2004h ----> 10 &n (int有4个字节)
指针:变量的指针就是变量的地址(内存中的首地址)
指针变量:指针变量是一种特殊的变量,它的数据是内存的一个地址(unsigned long int),通过这个内存可以找到此内存本身存储的数据。如:int a = 10 int *p; //定义一个指针变量,指针的类型是int p = &a; //p是指针变量,p的数据是a的地址 p p的数据是地址 地址指向的值 2005h ---> 2001h --> 10 *p --> a的地址里面的数据 --> 10
*是“间接运算符”,用来定义一个指针变量,以及取指针变量指向的数据。赋给指针变量的值必须是地址常量或变量,不能为普通整数(可以为 0表示空指针),此地址中存放的数据类型必须与指针类型相符。如
1)int *p; //定义指针变量 p int *p = &a; ---> int *p; p=&a 2)printf("%d\n",*p); //取指针变量p指向的数据 *(&a) --> a //取&a这个地址(指针)指向的数据
*p = 5; //把指针的值修改为5,n == 5
int n; char c; void *p; p = &n; p = &c;
注意:void类型指针不能直接输出或运算,需要转换成指定类型。
void *pv; int *pint, n; pv = &n; pint = (int*)pv; //强制类型转换
int n[5]; int *p1=n,*p2=&n[3]; p1-p2 --> 3
注:不是同一数组的指针相减没有意义。
int n, *p=&n; //int是4字节 p+1 --> (n的地址 + 4 )的新指针
int a=10,b=20,c=30; int *p=&b; *p --> 20 *p++ --> 20,但此时p指向 p+1的 地址(4个字节)
pq p指向的元素在q指向的元素之后,返回1,否则0 p==q 两者指向同一元素,返回1,否则0 p!=q 不指向同一元素,返回1,否则0
p==null 空指针返回1,否则0 p!=null 不是空指针返回1,否则0
int a = 3, b = 10; const int *p = &a; *p = 5; //报错,不能修改 p = &b; //可以修改
int a=3, b=10; int *const p = &a; p = &b; //报错,不能修改 *p = 5; //可以修改
理解:看const后面是什么,如果是*p则表示指针的值不能修改,如果是p则表示指针不能更换指向。
指针与结构体 结构体类型的指针变量,定义方式为struct 结构体类型 *变量名; struct student stu, *p; p = &stu;
stu.name; (*p).name; p->name; //指针专用,指向运算符,优先级比单目运算符高 p->age++; //先获得age成员,再++
void change(int *p){ *p = 20; } int main(){ int a=5; change(&a); //传入指针,在函数里a的值被修改 }
void print(int n){ //回调函数,打印信息 printf("回调函数调用:%d\n",n); } void loop(int max, int (*p)(int n)){ //遍历1..n,如果7的倍数就打印信息 int i; for(i=0;i
;>
;>
;>
;>
;++i){>
;++i)>
;++i){>