数据类型
用不同数据类型所定义的变量所占空间大小不一样,定义的变量不是保存于数据类型中,而是因为只有定义了该数据类型的变量才能保存数据。
一、整型
1、整型(int) 四字节,默认有符号(-231-231-1),无符号加unsigned(0-232-1)(十位数);
2、短整型(short int) ,两字节(-215-215-1)(五位数);
3、长整型(long int) ,四字节(同int,在目前的操作系统中几乎没有区别);
4、长长整型(long long int), 八字节(-263-263-1),无符号(0-264-1);
二、浮点型
1、单精度浮点数(float),四字节,保留小数点后6位
2、双精度浮点数(double),八字节,保留小数点后15位
int为一个32为的存储单元,long long为64为的存储单元
1 B/byte(字节) = 8 bit(比特)(位)
1 KB(千字节) = 1024 B/byte(字节)
1 MB = 1024 KB
1 GB = 1024 MB
1TB =1024 GB
1 PB = 1024 TB
1 EB = 1024 PB
三、字符型
char,用于储存字符,和int很像,可用ASCII码来储存字符
char grade=’A’; char grade=65;
' '单引号为字符,eg:char a='a';
" "双引号为字符串,eg:char a=“asd”;编译器会自动给字符串结尾添加’\0‘来作为字符结束标识,strlen函数中不统计\0,但是\0在内存中占据空间。
除此之外,还有转义字符,通过反斜杠来完成相关操作,如果要特殊字符转字面字符需要另外添加反斜杠,转义字符在字符串中占空间,但是只计算一个长度,\0不计长度。*
四、变量和常量
作用域(scope),程序设计概念,通常来说,一段程序代码中所用到的名字并不总是有效/可用的,而限定这个名字的可用性的代码范围就是这个名字的作用域。
生命周期:变量的生命周期指的是变量的创建到变量的销毁之间的一个时间段
#include <stdio.h> int global = 2019;//全局变量 int main() { int local = 2018;//局部变量 return 0; }
分支及循环语句
一、分支语句
1、if语句
语法结构:
if(表达式) 语句; if(表达式){ 语句列表1 } else{ 语句列表2; } //多分支 if(表达式1){ 语句列表1; } else if(表达式2){ 语句列表2; } else{ 语句列表3; }
表达式部分为真则执行语句(0为假,非0为真),尽量在每个分支语句后都加{},否则只会执行分支后第一条语句。
else在没有括号的情况下遵循就近原则所以在多重if语句嵌套使用情况下一定要加括号!
2、switch语句
switch作为分支结构常用于多分支的情况,可以简化多重if语句嵌套的情况。
语法结构
switch(表达式A){ case 常量表达式1: 语句1; break; case 常量表达式2: 语句2; break; …… case 常量表达式n: 语句n; break; default: 语句n+1; break; }
其中
1、case后第一句不可定义变量,必须跟常量或者常量表达式,并且不可相同;
2、break在语句中可以起到划分作用,不可省略,否则无法实现分支功能;
3、default语句不应该省略,一般推荐位语句列表末尾;
4、switch语句结束条件:①遇到break;②执行到语句列表末尾。
二、循环语句
1while语句
while(表达式){ 循环语句; }
注:在循环语句中break的作用是停止后期所有循环,continue的作用是终止本次循环,开始下一次循环的判断。
2、for语句
for(表达式1;表达式2;表达式3){ 循环语句; }
表达式1为初始化部分,用于初始化循环变量,当表达式1为空时直接进入循环;
表达式2 为条件判断部分,用于判断循环是否终止,当表达式2为空时为死循环;
表达式3为调整部分,用于循环条件的调整 。
注:建议使用“前闭后开”来限定区间范围。
for(i=0; i<10; i++){ a[i]=i; }
3、do while语句
do{ 循环语句; }while(表达式);
循环体至少执行一次,while之后记得加分号。
二分查找函数循环实现范例:
int bin_search(int arr[], int left, int right, int key) { int mid = 0; while(left<=right){ mid = (left+right)>>1; if(arr[mid]>key) { right = mid-1; } else if(arr[mid] < key) { left = mid+1; } else { return mid;//找到了,返回下标 } } return -1;//找不到 }
函数
一、库函数
C语言基础库中的函数,在添加头文件后可直接调用。
二、自定义函数
1、函数组成
由函数名、返回值类型、函数参数以及函数体组成。
实参:真实的传入函数的变量,在被调用时会发生临时拷贝,并非把原来的变量直接放入函数中,只是把实参的数据拷贝给形参。
形参:函数名括号后的变量,因为形参只有在被调用的时候才被实例化并分配空间(形参实例化),在被调用过后即被销毁,只在该函数中有效(局部变量),所以叫形参。
//函数定义 double Add(double x, double y){ return x+y; } //函数声明 double Add(double x, double y);
2、函数调用
分为传值调用与传址调用,其中传址调用是把函数外部创建的内存地址传递给函数,可以真正与原值建立起联系,直接操纵函数外部的变量。
函数也可以进行嵌套调用以及链式访问。
嵌套调用样例:
#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; }
链式访问样例:
#include <stdio.h> #include <string.h> int main() { char arr[20] = "hello"; int ret = strlen(strcat(arr,"bit")); printf("%d\n", ret); return 0; }
3、函数递归
** 程序自身调用被称为递归,把复杂问题层层转化为与原问题类似的小问题,函数在调用自身的情况下存在不合法递归(即无限次的递归导致栈溢出)。
所以在使用递归的时候一定要有递归出口,否则会陷入死循环导致栈溢出!
**注:**栈结构为电脑存储的一部分,从高地址处向下开辟存储空间,与用于开辟动态存储空间的堆相向开辟(堆为从低地址出向上开辟存储空间),而函数调用将会形成栈帧,函数返回后自动释放栈帧结构,在此过程中,该函数定义的所有局部变量都在该函数的栈帧内进行空间开辟。
样例:求n的阶乘
int factorial(int n) { if(n <= 1) return 1; else return n* factorial(n-1); }
递归与迭代
递归在使用过程中由于频繁进行函数调用,且每次调用都需要时间成本以及空间成本,所以递归程序简单,但是可能导致递归效率变低的问题,而迭代方案通过对变量值进行替换所以不会造成栈溢出,解决了效率低下的问题。
样例(求斐波那契数列第n个的值):
//递归实现 int fibrec(int n){ if(n<=2) retuen 1; else return fibrec(n-1)+fibrec(n-2); } //迭代实现 int fibite(int n){ int fir=1,sec=1,thd=2; if(n<=2) return 1; else{ while(n>2){ fir=sec; sec=thd; thd=fir+sec; n--; } return thd; } }
数组
一、一维数组的创建与初始化
创建数组时数组空间为整体开辟整体释放,在内存中是连续存放,在定义时就已经确定数组大小(下标不可为0),且不可被整体赋值。在数组的创建过程中,如果进行了初始化则可不指定数组的大小,多维数组按照一维数组进行理解。
数组传参发生降维,降维成指向其(数组)内部元素类型的指针。
数组名一般情况下都指的是首元素的地址,但如果sizeof()单独出现以及&后跟数组名时表示的是整个数组
int s[5]; //表示数组首元素地址 printf("%d\n", sizeof(s+1));//结果为4/8,指针的具体大小根据编译器的不同大小不同 //表示整个数组 printf("%d\n", sizeof(s));//结果为20
二、数组传参(函数)
由于在传参过程中如果拷贝整个指针会导致效率大大降低甚至导致栈溢出,所以数组传参要发降维问题,函数内数组作为参数时,实参为首元素地址,形参为指针。
在访问结构体成员时也同样要发生类似的操作,用指向结构体的指针来指代结构体。
typedef struct node{ int a; int b; }point; void pop(int* p){ } int main(){ point a; int* p=a; pop(p); return 0; }
传参样例:
//用数组的形式传递参数,不需要指定参数的大小,传的只是数组首元素的地址。 void test(int arr[]) {} //也可以传入数组大小 void test(int arr[10]) {} //数组降维,用指针进行接收,传的是数组首元素的地址 void test(int *arr) {} //二维数组传参,第一个方括号可以空,但是第二个不可以空 void test(int arr[][5]) {} void test(int arr[4][5]) {} //传过去的是二维数组的数组名,即数组首元素的地址,也就是第一行的地址,第一行也是个数组,用一个数组指针接收(比较少用) void test(int (*arr)[5]) {}
三、字符数组
char a[]={'a','x','d'}; //此处由于结尾没有'\0',strlen的机制是遇到'\0'即停止,所以在结尾没有'\0'时为随机数 //strlen(a)为随机数 //sizeof(a)为3 char a[]={'a','x','d','\0'}; //strlen(a)为3 //sizeof(a)为4 char* a="axd";//或char a[]="axd" //直接通过""定义字符串时,会自动在结尾补'\0',不需要自行补充,但'\0'依旧会占据一个字节 //strlen(a)为3 //sizeof(a)为4 char c[5]={'a', 'b', '\0', 'c', '\0'}; printf("%s", c);//结果为ab,因为字符串结束标志位'\0'
操作符
一、运算优先级
注:①++/–高于解引用;
②解引用高于±/
③±/高于位运算;
④位运算高于+=、-=、/=、*=;
%操作两边必须是整数。**
二、二进制中的操作符
1、位运算基本介绍
与运算:&
同1则1,否则为0;
或运算:|
同0为0,否则为1
非运算:~
1取0 0 取1
异或运算:^
两者相等为0,不等为1
移位运算操作符:<< 左移 ; >> 右移
①<<左移:左边抛弃末尾补0;负数对反码的补码进行移位操作;相当于乘2;
②>> 右移:有符号的补符号位**,无符号的补0;相当于除以2。**
2、反码与补码
反码:正数的反码为原码本身,负数反码符号位不变,剩余的数字位取反;
补码:正数的补码为原码本身,负数的补码为反码+1
三、隐式类型转换
隐式类型转换的原因:参与计算的数据如果类型不同无法直接进行计算。
整型提升:有符号的补符号位,无符号的补0(符号位为最外面的那位)
指针
指针变量是个变量,指针本身是个地址,用于存放内存单元的地址。
指针时用来存放地址的,指针类型的变量无论指向目标是什么类型,指针本身在32位平台所占大小都为4个字节,在64位平台是8个字节
#include <stdio.h> int main() { int a = 10;//在内存中开辟一块空间,左值为空间,右值为内容 int *p = &a;//type* p //这里我们对变量a,取出它的地址,可以使用&操作符。 //将a的地址存放在p变量中,p就是一个之指针变量。 return 0; }
一、指针的解引用
1、对指针的解引用只能看到sizeof(type)个字节的数据;
2、按字节为单位,数据有高权值和低权值之分,地址有低地址和高地址之分;
3、数据低权值位在低地址处即为小端存储,反之则为大端存储。
二、野指针
概念
指向的位置是不可知的指针。
规避
1、指针在定义时就进行初始化;
2、避免指针越界(eg:注意循环时循环次数的限制);
3、指针使用完即进行指针指向空间释放;
4、避免返回局部变量的地址;
5、指针使用前检查其有效性。
三、指针运算
1、指针±整数,等价于±sizeof(type);
2、指针-指针,两指针必须同一类型,一般用于数组与字符串求其两地址间相隔的单元格数,否则无效(指针+指针为非法操作);
3、指针的关系运算。
4、指针和数组都可以用中括号或者解引用(二者互通)。
四、字符指针
1、字符指针
在指针的类型中我们知道有一种指针类型为字符指针 char ;
一般使用方法*
int main() { char ch = 'w'; char *pc = &ch; *pc = 'w'; return 0; }
用char*指针指向字符串
int main() { const char* pstr = "hello bit."; printf("%s\n", pstr); return 0; } //上述代码中把字符串hello bit.的首地址放入了指针中
需注意字符串可以以字符数组的形式给出,但是此时的字符数组附有存储功能,而字符指针具有常量属性,指向的是常量区的内容,因此不可被修改,可以写作:
const char* str="hello world";//从上图表示不可被修改
也正因为这个原因 C/C++会把常量字符串存储到单独的一个内存区域,当几个指针指向同一个字符串的时候,他们实际会指向同一块内存。但是用相同的常量字符串去初始化不同的数组的时候就会开辟出不同的内存块。
#include <stdio.h> int main() { char str1[] = "hello world."; char str2[] = "hello world."; const char *str3 = "hello world."; const char *str4 = "hello world."; if(str1 ==str2) printf("str1 and str2 are same\n"); else printf("str1 and str2 are not same\n"); if(str3 ==str4) printf("str3 and str4 are same\n"); else printf("str3 and str4 are not same\n"); return 0; }
2、const
在此说一下const的一些知识点:
① const修饰的变量不能被直接修改,但是可以通过指针在进行类型强转来修改(只是可以但是完全没必要);
② const修饰指针,表示不可以通过指针来修改所指目标;
③ const能用则用,会很好的保护数据,
const的作用:
① 写给编译器看,提前发现可能错误的修改;
② 写给程序员看,提示该变量不建议修改。
const int* p=&a; *p = 20; //错误,*p所指的值不可以修改 p = &n;//正确,*p的指向可以修改 int* const q = &m; *q = 20;//正确,此时const修饰的是q,此时q所指向的值可以进行修改 q = &t;//错误,由于const修饰的是q,此时的q的指向不可以进行修改 const int a=10; //若const a=10,编译器也会默认为a是int类型的 int *P=(int*)&a; //注意需要强制&a前需要加int*类型强制类型转换(&a的原本类型为const int*) *P=12;
五、指针数组
指针数组本质上是数组,该类数组内存放的的元素是指针
int* arr1[10]; //整形指针的数组 char *arr2[4]; //一级字符指针的数组 char **arr3[5];//二级字符指针的数组
六、数组指针
1、数组指针定义
指针数组本质上是指针,该类指针指向的是一个数组。
int (*p)[10]; //解释:p先和*结合,说明p是一个指针变量,然后指着指向的是一个大小为10个整型的数组。所以p是一个 指针,指向一个数组,叫数组指针
2、数组指针&数组
首先需要看的是数组名与&数组名(可以等价于数组指针)之间的关系
#include <stdio.h> int main() { int arr[10] = { 0 }; printf("arr = %p\n", arr); printf("&arr= %p\n", &arr); printf("arr+1 = %p\n", arr+1); printf("&arr+1= %p\n", &arr+1); return 0; }
根据上面的代码我们发现,其实&arr和arr,虽然值是一样的,但是意义应该不一样的。实际上: &arr 表示的是数组的地址,而不是数组首元素的地址。(细细体会一下)本例中 &arr 的类型是: int(*)[10] ,是一种数组指针类型数组的地址+1,跳过整个数组的大小,所以 &arr+1 相对于 &arr 的差值是40
七、数组传参,指针传参
1、一维数组传参
数组传参会发生降维,最终传入的是首元素的地址(指针),并利用此来访问数组内其他元素。
#include <stdio.h> void test(int arr[])//ok {} void test(int arr[10])//ok {} void test(int *arr)//ok {} void test2(int *arr[20])//不ok {} void test2(int **arr)//不ok {} int main() { int arr[10] = {0}; int *arr2[20] = {0}; test(arr); test2(arr2); }
2、二维数组传参
二维数组传参,函数形参的设计只能省略第一个[]的数字。
因为对一个二维数组,可以不知道有多少行,但是必须知道一行多少元素。
void test(int arr[3][5])//ok {} void test(int arr[][])//不ok {} void test(int arr[][5])//ok {} void test(int *arr)//不ok {} void test(int (*arr)[5])//ok {} void test(int* arr[5])//不ok {} void test(int **arr)//ok {} int main() { int arr[3][5] = {0}; test(arr); }
3、一级指针传参
需要传入一个地址
#include <stdio.h> void print(int *p, int size) { int i = 0; for(i=0; i<size; i++) { printf("%d\n", *(p+i)); } } int main() { int arr[10] = {1,2,3,4,5,6,7,8,9}; int *p = arr; int size = sizeof(arr)/sizeof(arr[0]); //一级指针p,传给函数 print(p, size); return 0; }
4、二级指针传参
#include <stdio.h> void test(int** ptr) { printf("num = %d\n", **ptr); } int main() { int n = 10; int*p = &n; int **pp = &p; test(pp); test(&p); return 0; }
#include <stdio.h> void test(int** ptr) { printf("num = %d\n", **ptr); } int main() { int n = 10; int*p = &n; int **pp = &p; test(pp); test(&p); return 0; }
八、函数指针
1、函数指针定义
函数指针是指指向函数而非指向对象的指针。像其他指针一样,函数指针也指向某个特定的类型(特定的函数类型)。函数类型由其返回类型以及形参表确定,而与函数名无关 。
代码在电脑中同样占据内存空间,所以具有存储地址,而代码部分在电脑中也是不可被修改的类似字符串常量。
在函数中,函数名单独时即为函数的地址(eg:main=&main),所以在用指针调用函数时,可以直接用指针调用不需要加
Type (*pFunc)(datatype args); //pFunc为函数指针,Type为数据类型,参数(datatype args)可以有多个,也可以没有。
bool max(int a, int b) { if (a>b) { return a; }else{ return b; } } void Test() { bool (*pFunc)(int, double); pFunc = max; cout << max(5, 10) << endl; }
九、函数指针数组
指针指向一个数组 ,数组的元素都是函数指针
使用方法: 把几个相同类型的函数地址放到一个数组中,这个数组就是函数指针的数组。
十、回调函数
解释:调用库中函数,但是库中函数需要编写程序时编写一个调用函数,该库中的函数为回调函数。也就是说一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。
回调函数必须在中间函数以及回调函数同时具备时才可以实现。
回调函数就是回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。
结构体
1、基本定义
** **注:①结构体不可以自引用!但可以在结构体内定义该结构体类型的指针!
②定义结构体本质是新增一种类型;
③结构体传参要传结构体地址(指针),以此提高效率。
struct node1{ int a; int b; }; typedef struct node2 { int c; int d; }node2;//通过typedef为结构体变量定义一个别名node2,在以后使用中可以使用别名以此提高编写效率 int main(){ struct node1 s;//用结构体定义变量 node2 q;//用别名定义变量 }
**注:**结构体不可以自引用!但可以在结构体内定义该结构体类型的指针!
struct Node { int data; struct Node next; };//错误 struct Node { int data; struct Node* next; };//正确 typedef struct Node { int data; struct Node* next; }Node;//正确
2、结构体变量的定义和初始化
.
struct Point1 { int x; int y; }p1; //声明类型的同时定义变量p1 struct Point p2; //定义结构体变量p2 typedef struct Point2 { int x; int y; }p; p p1; p p2;
在定义结构体变量时,可以在初始化的部分定义其内容,也可以在之后定义。
typedef struct Point2 { int x; int y; }p; p p1; p1.x=1; p1.y=2;//可以直接用结构体类型的变量进行定义 typedef struct Point2 { int x; int y; }p; p* p2=(p*)malloc(sizeof(p)); p2->x=1; p2->y=2;//定义一个指向结构体的指针并为其分配空间即可进行定义
3、结构体的内存对齐(结构体的占用大小的计算)
结构体内存空间占用的大小并不是单纯的元素相加,而是通过浪费一定量的空间来换取目标数据读取速度的加快
计算方式:
① 第一个成员在与结构体变量偏移量为0的地址处。
② 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
(起始偏移量要能整除该成员的对齐数)
对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。
VS中默认的值为8
③ 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
④ 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处(即结构体大小),结构体的整
体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
struct S1 { char a; int b; char C; }; printf("%d\n", sizeof(struct S1));
char 为1个字节, int 为4个字节;
char a 从0偏移开始,占用一个字节;偏移量为1,接下来存放 int b,偏移量1不是对齐数4 的整数倍,所以向后继续偏移一直到4,4是对齐数4的整数倍,所以将int b存放在偏移地址为4处,占用4个字节;偏移量变为8,存放 char c ,占一个字节,偏移量9不是最大对齐数4的整数倍,所以向后继续偏移直到偏移处为12的地方。
位断
位段的声明和结构是类似的,有两个不同:
1.位段的成员必须是 int、unsigned int 、signed int或者char(同属于整型家族);
2.位段的成员名后边有一个冒号和一个数字。
struct A { int _a:2; int _b:5; int _c:10; int _d:30; };
枚举
enum Day//星期 { Mon, Tues, Wed, Thur, Fri, Sat, Sun //最后一个不加逗号 }; enum Sex//性别 { MALE, FEMALE, SECRET };
与宏定义相比枚举的优点:
① 增加代码的可读性和可维护性;
② 和#define定义的标识符比较枚举有类型检查,更加严谨;
③ 防止了命名污染(封装);
④ 便于调试;
⑤ 使用方便,一次可以定义多个常量 。
联合体
联合也是一种特殊的自定义类型
这种类型定义的变量也包含一系列的成员,特征是这些成员公用同一块空间(所以联合也叫共用体), 联合的成员是共用同一块内存空间的,这样一个联合变量的大小,至少是最大成员的大小(因为联合至少得有能力保存最大的那个成员)。
//联合类型的声明 union Un { char c; int i; }; //联合变量的定义 union Un un;
**注:**联合体需要考虑内存对齐,要求为最大内存数的整数倍。
动态内存管理
程序开始运行后在堆上开辟大量空间(数组之类的空间开辟在栈上进行),而在堆上开辟的空间使用完毕后需要在使用完成后由free函数进行释放,然后令指向该空间的指针指空,如果只申请不释放会造成内存泄漏问题。
动态申请空间主要涉及三个函数:malloc函数,calloc函数,relloc函数。
void* malloc (size_t size);
只申请空间,不对空间进行初始化,传入的参数size为要开辟的空间大小;
void* calloc (size_t num, size_t size);
申请空间,与malloc唯一的不同之处在于calloc会初始化为0,传入的参数size为单个空间的大小,参数a为所需要的单个空间的数量;
void* realloc (void* ptr, size_t size);
将分配size个大小的空间,然后在调整原内存空间大小的基础上,将原来内存中的数据移动到新的空间,返回值为调整之后的内存起始位置。
由于realloc可能会申请失败返回NULL所以不建议直接用原指针接收返回地址,正确使用方法为:
int* ptr = (int*)malloc(100); int* p = NULL; p = realloc(ptr, 1000); if (p = !NULL) { ptr = p; }
内存释放操作
int* p=(int*)malloc(100); ...... free(p); p = NULL;
柔性数组
1、定义
在结构体内大小为0(a[0])或空(a[])的数组(必须为结构体内最后一个元素且不能是唯一元素)
这样可以在结构体内具有一个变长数组包含柔性数组成员的结构用malloc ()函数进行内存的动态分配,并且分配
的内存应该大于结构的大小,以适应柔性数组的预期大小 ,sizeof 返回的这种结构大小不包括柔性数组的内存 。
2、使用方法
typedef struct st_type { int i; int a[0];//柔性数组成员 }type_a; printf("%d\n", sizeof(type_a));//输出的是4 //初始化 type_a *p = (type_a*)malloc(sizeof(type_a)+100*sizeof(int)); p->i = 100; for(i=0; i<100; i++) { p->a[i] = i; } free(p);