C语言笔记
一维数组
数组是一组相同类型元素的集合。数组的创建方式:
type_t arr_name [const_n]; //type_t 是指数组的元素类型 //const_n是一个常量表达式,用来指定数组的大小
数组的下标运算符中不能放变量,只能放常量
int arr[10]//正确的 int n=10; int arr[n];//错误的
数组是使用下标
[ ]
来访问的,下标是从0开始。数组的大小可以通过计算得到。
int arr [10]; int sz=sizeof(arr)/sizeof(arr[0]);//计算数组的大小
**一维数组在内存中是连续存放的 **
随着数组下标的增长,地址是由低到高变化的
数组名是数组首元素的地址
int arr[10]={1,2,3,4,5,6,7,8,9,10}; int *p=arr;//其是数组首元素的地址
二维数组
-
二维数组的创建
int arr[3][4]//代表的是三行四列 char arr[3][4]///在创建数组名称的前面定义二维数组类型
-
二维数组的初始化
int arr[3][4]={1,2,3,4,5}//其中会默认按照顺序进行排列 int arr[3][4]={{1,2},{4,5}};//其中{}代表的行,而在其中的数字代表的是列数字 int arr[][4]={{1,2},{4,5}};//其中行数可以不表示,但是列数必须要表示
二维数组在内存中也是连续存放的
一行内部连续,跨行也是连续的,每个整形所占的内存是四个字节
数组为函数参数
-
冒泡排序
将数组内的的数变为升序排序即为冒泡排序
数组传参的时候,传递的其实是数组首元素的地址
void bubble_sort(int arr[])//形参arr本质是指针 { int sz =sizeof(arr)/sizeof(arr[0]);//计算数组元素的大小,但其传来的只是首元素的地址,故求得的sz是1,所以应该将其放在函数为计算。 }
在函数部分写成数组的形式:*int arr[ ]表示的依然是一个指针:int arr
-
数组名
数组名是数组首元素的地址,但是有两个例外:
1.sizeof(数组名)–数组名表示整个数组–计算的是整个数组的大小单位是字节
2.&数组名–数组名表示整个数组–取出的是整个数组的地址
printf("%p\n",arr); printf("%p\n",&arr);//虽然两者打印的地址是一样的,但是所代表的含义是不同的
操作符
-
算术操纵符
其中包含**+ - * / %**
对于**/操作符,如果两个操作数都为整数时,其执行的是正数运算**,而进行运算时需要得到的是小数,那么所运算的数至少有一个是浮点数,例如
float a=6.0/5; float a=6/5.0; printf("%f",a);
对于**%操作符的两个操作数必须为整数**,返回的是整除之后的余数
-
移位操作符
**<< **左移操作符
>> 右移操作符
移动规则(二进制)
左移操作符:左边抛弃、右边补0
右移操作符:1.算术移位:左边用原该值的符号位填充,右边丢弃
2.逻辑移位:左边用0填充,右边丢弃
-
位操作符
按位与“ & ”:
运算符 “ & ” 的作用是:把参见运算两个数所对应的二进制位分别进行“与”运算,即:两位同为“1”,结果才为“1”,否则为0。例子如下:
00000000000000000000000000001100//12 00000000000000000000000000001010//10 00000000000000000000000000001000//12&10=8
按位或“ | ”:
运算符 “ | ” 的作用是:把参见运算两个数所对应的二进制位分别进行“或”运算,即:两位同为“0”,结果才为“0”,否则为1。例子如下:
00000000000000000000000000001100//12 00000000000000000000000000001010//10 00000000000000000000000000001110//12|10=14
按位异或“ ^ ”:
按位异或运算规则为:把参见运算两个数所对应的二进制位分别进行“异或”运算,相同为0,相异为1。例子如下:
00000000000000000000000000001100//12 00000000000000000000000000001010//10 00000000000000000000000000000110//12^10=6
按位异或的使用技巧
在不引用第三者变量的情况下,使得a和b中的数字能够调换
int a=3; int b=5; a=a^b; b=a^b; a=a^b; printf("a=%d b=%d",a,b);//其中使用三次^即可将数值调换
-
逻辑操作符
&& 逻辑与
| | 逻辑或
-
三目操作符
a?b:c 若为真则将值赋给了b,若为假则将值赋给了c
int a = 1; int b = 2; int c = 0; c = a < b ? a : b; // c = 1
-
逗号表达式
**1、逗号表达式是C语言*优先级最低*的运算符。
2、逗号表达式是*左结合性*(即按从左到右顺序运算)的运算符。
#include<stdio.h> void main(){ int x, y, z, o; o = ( x = 3, y = 4, z = 5 ); printf("x=%d,y=%d,z=%d,o=%d\n",x,y,z,o); } //打印的结果是x=3,y=4,z=5,o=5 //这就代表了逗号表达式的值由最后一个表达式来赋予的
-
点操作符
通常与struct函数相结合,使得能够引用来打印
#include <stdio.h> struct student //结构体类型的说明与定义分开。声明 { //结构体成员(变量) int age; /*年龄*/ float score; /*分数*/ char sex[20]; /*性别*/ };//结尾需是分号 int main () { struct student a={ 20,79,'male'}; //定义 //结构体变量名.成员名 printf("年龄:%d\n", a.age); printf("分数:%.2f\n", a.score); printf("性别:%s\n", a.sex); return 0;
其中的部分还可以用指针的形式来写
int main () { struct student a={ 20,79,'male'}; //定义 struct student*pa=&a; //结构体指针->成员名 printf("年龄:%d\n",pa->age); printf("分数:%.2f\n",pa->score); printf("性别:%s\n",pa->sex); return 0;
-
sizeof操作符
计算变量/类型所占内存大小,单位是字节
int main() { char str[]="hello bit"; printf("%d %d\n",sizeof(str),strlen(str)); //10 9 return 0; } //strlen-函数-求字符串长度,找\0 //sizeof-操作符-计算变量/类型所占内存大小,单位是字节
整形提升
- 什么是整形提升
表达式中的字符和短整形操作数在使用之前被转换成普通整形
int
,这种转换叫做整形提升
整形提升针对的类型小于整形的
char
,short
char
占用1字节空间,short
占用2字节空间,在运算时都会提升为占用4个字节的int
类型
所以C的整型算术运算总是至少以缺省整型类型的精度来进行的。换而言之整形算术运算都至少以
int
类型计算的
- 为什么会有整形提升
通用CPU(general-purpose CPU)是难以直接实现两个8比特字节直接相加运算(虽然机器指令中可能有这种字节相加指令)。所以,表达式中各种长度可能小于int长度的整型值,都必须先转换为int或unsigned int,然后才能送入CPU去执行运算。
- 如何进行整形提升
负数的整形提升
char c1 = -1,char类型占1字节,也就是它的二进制补码只有8个比特位 : 1111111 因为 char 是有符号位的 char ,所以整形提升的时候,高位补符号位1 提升之后的效果为: 11111111111111111111111111111111
正数的整形提升
char c2 = 1; 变量c2的二进制位(补码)中只有8个比特位: 00000001 因为 char 为有符号的 char ,所以整形提升的时候,高位补充符号位,即为0 提升之后的结果是: 00000000000000000000000000000001
无符号的整形提升:高位补0
-
例子1
#include<stdio.h> int main() { char a = 3; char b = 127; char c = a + b; printf("%d\n", c); return 0; }
char a = 3
,首先把3
放到a
中,3是int
类型,3的二进制序列为:00000000000000000000000000000011
需要把3
放到char
中,int
是32个比特位,char
是8个比特位,所以接下来需要进行截断
:将低8个比特位放到char
中,所以此时a
中为:00000011
char b = 127
也是一样
127
为00000000000000000000000001111111
截断
为011111111
char c = a + b
,接下来要进行整型提升
当前char
为有符号的char
,所以对于a
就高位补0
,为:00000000000000000000000000000011
同理,b
整形提升后为:00000000000000000000000001111111
接下来相加为
00000000000000000000000010000010
,将这个32位二进制放到c
中,截断为10000010
printf("%d\n", c)
中,%d
是打印十进制的数,所以还需整形提升,此时c
为10000010
,符号位为:1
,整形提升高位补1
,最终为11111111111111111111111110000010
(补码),其原码为10000000000000000000000001111110
,所以这个数字输出为-126
-
例子2
int main() { char c = 1; printf("%u\n", sizeof(c)); printf("%u\n", sizeof(+c)); printf("%u\n", sizeof(-c)); return 0; }
c
是char
类型,sizeof(c)
值为1
+c
.-c
中,c
都参与计算,所以整形提升为int
类型,sizeof(+c)
,sizeof(-c)
值都为4
算术转化
如果某个操作符的各个操作数属于不同的类型,那么除非其中一个操作数的转换为另一个操作数的类型,否则操作就无法进行。下面的层次体系称为寻常算术转
1. long double
2. double
3. float
4. unsigned long int
5. long int
6. unsigned int
7. int
如果某个操作数的类型在上面这个列表中排名较低,那么首先要转换为另外一个操作数的类型后执行运算。
int main()
{
int a = 3;
float b = 3.5;
float c = a + b;
//算术转换,int-->float
//结果为 6.5
return 0;
}
指针
-
指针类型的意义
指针类型决定了指针进行解引用操作的时候,能访问空间的大小。
指针类型决定了指针的步长(指针走一步可以走多远)。
-
野指针
1.指针未初始化
#include<stdio.h> int main() { int* p; *p = 10; return 0; }
2.指针跨界访问
#include<stdio.h> int main() { int arr[10] = { 0 }; int i = 0; int* p = arr; for (i = 0; i <= 10; i++) { *p = i; //i=10时越界 } return 0; }
3.指针指向空间的释放
#include<stdio.h> int* test() { int a = 10; return &a; //&a=0x0012ff40 } int main() { int* p = test(); return 0; }
野指针的避免
- 指针初始化
2. 小心指针越界
3. 指针指向空间释放即时置NULL
4. 避免返回局部变量的地址
5. 指针使用之前检查有效性
初始化为空指针。如果定义指针的时候实在不知道赋什么值,可以先将其定义为空指针,即int*p=NULL;,NULL是代表空指针的意思,后面如果用到指针的话再让指针指向具有实际意义的地址,然后通过指针的解引用改变其指向的内容。
#include<stdio.h> int main() { int* p=NULL; int b = 8; p = &b; *p = 100; printf("%d",*p); return 0; }
- 指针初始化
-
指针的运算
指针+ - 整数:
指针-指针:
注:指针+指针是没有意义的
指针的关系运算(比较大小):
-
标准规定
允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许与指向第一个元素之前的那个内存位置的指针进行比较。
- 指针与数组
int main()
{
int arr[10]={1,2,3,4,5,6,7,8,9};
int * p= arr;
printf("%d",arr[2]);
printf("%d",*[p+2]);
printf("%d",2[arr]);
printf("%d",p[2]);
//上面四种写法所打印出来的数字都是一样的
//当系统识别时都会转化成*(arr+2)或*(p+2)
}
[ ]是一个操作符,2和arr是两个操作数
例如:a+b=b+a
- 二级指针
**ppa通过对ppa中的地址进行解引用,这样找到的是pa, ppa其实访问的就是pa,然后对pa再进行解引用就找到a
int a=10;
int* pa=&a;//pa也是个变量,&pa取出pa再内存中的起始地址
int* *ppa=&pa;
- 指针数组
int main()
{
int arr[10];//整形数组-存放整形的数组就是整形数组
char ch[5];//字符数组-存放的是字符
//指针数组-存放指针的数组
int *parr[5];//整形指针的数组
char *parr[5];//字符指针的数组
}
- const修饰指针
巧记口诀:左定值,右定向,const修饰不变量(凉皮女孩)
char *p = "hello"; // 非const指针,
// 非const数据
const char *p = "hello"; // 非const指针,
// const数据
char * const p = "hello"; // const指针,
// 非const数据
const char * const p = "hello"; // const指针,
// const数据
结构体
- 结构体的定义与使用
结构体是一种构造数据类型
把不同类型的数据组合成一个整体
- 定义形式
struct stu{
char name[]; //姓名
int num; //学号
int age; //年龄
char group; //所在学习小组
float score; //成绩
};
注:大括号后面的分号不能少;
,这是个完整的语句
- 定义结构体类型的同时定义结构体变量
struct data
{
int day;
int month;
int year;
}time1,time2;
- 访问结构成员
成员访问运算符(.)
#include <stdio.h>
struct student //结构体类型的说明与定义分开。声明
{
//结构体成员(变量)
int age; /*年龄*/
float score; /*分数*/
char sex[20]; /*性别*/
};//结尾需是分号
int main ()
{
struct student a={ 20,79,'male'}; //定义
//结构体变量名.成员名
printf("年龄:%d\n", a.age);
printf("分数:%.2f\n", a.score);
printf("性别:%s\n", a.sex);
return 0;
}
运用指针的形式用操作符(->)
int main ()
{
struct student a={ 20,79,'male'}; //定义
struct student*pa=&a;
//结构体指针->成员名
printf("年龄:%d\n",pa->age);
printf("分数:%.2f\n",pa->score);
printf("性别:%s\n",pa->sex);
return 0;
}
函数传参的时候,参数是需要压栈的,如果传递一个结构体对象的时候,结构体过大,参数压栈的系统开销比较大,所以会导致性能下降。
结论:结构体传参的时候,要传结构体的地址。
- 结构体的声明
struct tag
{
member-list;
}variable-list;
- 特殊的声明
//匿名结构体类型
struct
{
int a;
char b;
float c;
}x;
struct
{
int a;
char b;
float c;
}a[20], *p;
- 结构体的自引用
struct Node
{
int data;
struct Node* next;
};
- 结构体变量的定义和初始化
struct Point
{
int x;
int y;
}p1; //声明类型的同时定义变量p1
struct Point p2; //定义结构体变量p2
//初始化:定义变量的同时赋初值。
struct Point p3 = {x, y};
struct Stu //类型声明
{
char name[15];//名字
int age; //年龄
};
struct Stu s = {"zhangsan", 20};//初始化
struct Node
{
int data;
struct Point p;
struct Node* next;
}n1 = {10, {4,5}, NULL}; //结构体嵌套初始化
struct Node n2 = {20, {5, 6}, NULL};//结构体嵌套初始化
- 结构体的对齐规则
第一个成员在与结构体变量偏移量为0的地址处。
其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。
VS中默认的值为8
结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整
体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
函数
-
switch函数:选择结构语句
switch的格式
switch(表达式) { case 整型数值1: 语句 1;break; case 整型数值2: 语句 2;break; ...... case 整型数值n: 语句 n;break; default: 语句 n+1;break; }
表达中可以是一个公式,也可以是一个变量,但是其输出或者表示的结果必须与下面的case相对应。
其后面加入break,是为了在对应的case语句执行完不会再接着执行。
default表示当情况都不满足时,可以直接跳出swicth的语句。
-
default关键词,开关语句中的“默认”分支;
默认”分支指的是:在switch语句中不满足所有case的情况下,会进default, 若不写则直接跳出switch。
#include<stdio.h>
int main() {
switch ()
{
case 1:
printf("结果等于1");
break;
case 2:
printf("结果等于2");
break;
case 3:
printf("结果等于3");
break;
default:
printf("结果一定不是1或2或3");
}
}
- rand函数:C语言中随机产生一个随机数的函数
rand产生一个0-0x7fff的随机数,即最大是32767的一个数
rand()函数每次调用前都会查询是否调用过srand(seed),是否给seed设定一个值,如果有那么它会自动调用srand(seed)一次来初始化它的起始值。如果没有之前没有调用srand(seed),那么系统会自动给seed赋初始值,即srand(1)自动调用它一次
包含的头文件是#include<stdlib.h>
利用rand函数可以表示出0~99的随机数
int num = rand() % 100;
利用rand函数可以表示出1~100的随机数。
int num = rand() % 100 + 1;
- srand函数:随机数发生器的初始化函数
此函数需要提供一个种子。如srand(1),用1来初始化种子
rand()产生随机数时,如果用srand(seed)播下种子之后,一旦种子相同,产生的随机数将是相同的。当然很多时候刻意让rand()产生的随机数随机化,用时间作种子 srand(time(NULL)),这样每次运行程序的时间肯定是不相同的,产生的随机数肯定就不一样了。
我们常常使用系统时间来初始化,使用time函数来获取系统时间,得到的值是一个时间戳,即从1970年1月1日0点到现在时间的秒数,然后将得到的time_t类型数据转化为(unsigned int)的数,然后再传给srand函数,用法如下:
srand((unsigned int)time(NULL));//我们在使用rand和srand时,主要使用的就是这一种初始化方法!!
如果仍然觉得时间间隔太小,可以在(unsigned)time(0)或者(unsigned)time(NULL)后面乘上某个合适的整数。 例如,srand((unsigned)time(NULL)*10)
time的参数传NULL表示不需要经过参数获得到的time_t数据,time函数原型如下
time_t time(time_t *t);
从声明中可以看出,time()函数返回值的数据类型是time_t。传递给time()函数的参数是指向time_t数据类型的指针。
- strcpy函数:覆盖拷贝
注:将source全覆盖拷贝到destination,会把’\0’也拷过去,且必须考虑destination的空间够不够,(destination的空间必须>=source的空间)
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<string.h>
int main()
{
char p1[] = "abcdef";
char* p2 = "hello";
strcpy(p1, p2);
printf("%s\n", p1);
printf("%s\n", p2);
return 0;
}
-
strlen函数:计算字符串的长度
计算的是字符串str的长度,从字符的首地址开始遍历,以 ‘\0’ 为结束标志,然后将计算的长度返回,计算的长度并不包含’\0’。
size_t strlen (const char* str);
- 函数的参数为------const char* str:字符指针
- 返回值的类型------size_t:无符号整数(即:unsigned int)
所包含在的头文件在**#include<string.h>**
-
freopen函数:用于重新打开一个已经打开的文件,从而改变该文件的指针和打开模式等属性。
FILE *freopen(const char *filename, const char *mode, FILE *stream);
freopen()函数的作用可以有以下三种情况:
- 重新设置文件指针:使用freopen()函数可以将一个已经打开的文件重新打开并赋予新的文件名和文件打开模式。这样做的目的是为了更好地控制文件指针,从而方便对文件进行各种操作(读、写、追加等)。
- 更改文件模式:使用freopen()函数也可以在不改变文件指针位置的情况下,更改文件打开模式。
- 重定向标准I/O流:freopen()函数还可以用于重定向标准输入输出流,从而实现程序中的输入输出重定向。
freopen()函数的第一个参数是要重新打开的文件名,第二个参数是新的打开模式,第三个参数是要重新打开的文件流指针。其中,第二个参数的取值及其含义如下:
“r”:以只读方式打开文件。
“w”:以写方式打开文件(如果文件已存在,则先清空文件)。
“a”:以追加方式打开文件,并将文件指针移到文件末尾。
“rb”、“wb”、“ab”:与上述相同,但是以二进制方式打开文件。
- AllocConsole函数:为当前的窗口程序申请一个Console窗口
BOOL AllocConsole(void);
可以配合freopen来使用控制台来调试输出,如下:
AllocConsole();
freopen("CONOUT$", "w", stdout);
-
memcpy函数:把信息从一处拷贝到另外一处
void *memcpy(void *destin, void *source, unsigned n);
以source指向的地址为起点,将连续的n个字节数据,复制到以destin指向的地址为起点的内存中。
函数有三个参数,第一个是目标地址,第二个是源地址,第三个是数据长度。其所包含的头文件在#inlclude<string.h>
自己所编写的memcpy函数,如下:
void my_memcpy( void* des,const void* src,int num) { int n = 0; //拷贝 for (n = 0; n < num; n++) { *(char*)des = *(char*)src; des = (char*)des +1; src = (char*)src + 1; } }
-
offsetof函数:返回结构体成员在内存中的偏移量。
size_t offsetof(type, m_name)
所包含的头文件是**#include<stddef.h>**
#include<stddef.h> struct S { char a; int b; float c; }; int main() { printf("%d\n", offsetof(struct S, a)); printf("%d\n", offsetof(struct S, b)); printf("%d\n", offsetof(struct S, c)); return 0; }
补充
- printf打印字符类型
字符 | 类型 | 含义 | 示例 |
---|---|---|---|
%c | char | 字符型,打印相应数值(ASCI码)对应的字符 | printf(“%c\n”,65);输出A |
%d、%i、%u | int/unsigned int | t有符号/无符号10进制整数,%i早期用法 | printf(“%u %04d\n”,123,123);输出 123 0123 |
%ld、%lu、%lld、%llu | long/unsigned long/long long/unsigned long long | 有符号long、无符号long、有符号long long、无符号long long类型打印 | printf(“%ld %lu\n”,12345678,12345678);输出12345678 12345678 |
%o | unsigned int | 无符号8进制(octal)整数(不输出前缀0) | printf(“0%o\n”,123);输出0173 |
%x、%X | unsigned int | 无符号16进制(hex)整数不输出前缀0x | printf(“0x%02x%#02X\n”,10123);输出0x0a 0X7B |
%f、%lf | float(double) | 单精度浮点数用f,双精度浮点数用lf(printf可混用,但scanf不能混用) | printf(“%.9f %.9lf\n”,0.000000123,0.000000123);输出0.000000123 0.000000123注意指定精度,否则printf默认精确到小数点后六位 |
%e、%E、%g、%G | float(double) | 科学计数法,使用指数(Exponent)表示浮点数此处"e”的大小写代表在输出时“e”的大小写. | printf(“%g\n”,0.000000123);输出123e-07 |
%s | char * | 输出字符串中的字符直至碰到’\0’ | printf(“%s\n”,“Hello\0 Word”);输出: Hello |
%p | void * | 以16进制形式打印地址 | int a;printf(“%p\n”,&a);打印变量a的地址 |
%% | 转义输出格式控制串中的% | printf(“%%\n”);输出% |
- 二进制表示形式
负整数的二进制形式,其实有三种:
原码:直接根据数据写出的二进制序列就是原码
反码:原码的符号位不变,其他位按位取反就是反码
补码:反码+1,就是补码
举例:负数-1(其存放在内存中存放的是二进制的补码)
原码:10000000000000000000000000000001
反码:1111111111111111111111111111111111110
补码:1111111111111111111111111111111111111
正整数的二进制形式里原码、反码、补码都是相同的
- 调试技巧
F5 ——启动调试
F9 ——设置/取消断点
F10 ——逐过程
F11 ——逐语句(更加的精细)
- const的基础概念
const名叫常量限定符,用来限定特定变量,以通知编译器该变量是不可修改的。习惯性的使用const,可以避免在函数中对某些不应修改的变量造成可能的改动。
-
字节与比特位的关系
1个字节=8个比特位
在32位的比特位环境里,一个地址的大小是4个字节
在64位的比特位环境里,一个地址的大小是8个字节
-
数据流
stdout,stdin, stderr是一个C程序中默认开启的三个数据流
stdout – 标准输出流 – 屏幕
stdin – 标准输入流 – 键盘
stderr – 标准错误流 – 屏幕
概念:我们写程序时,有时想把数据输出到屏幕上,有时想把数据输出到硬盘上,有时候想把数据输出到软盘上,有时想把数据输出到光盘上…
所以我们在写程序的时候会经常操控各种各样的硬件,硬件的不同,读写方式也不同,所以我们难道要懂得各种各样硬件的读写方式吗
所以在程序和硬件中间高度抽象了一个流的概念,我们只需要把数据丢给流,它帮我们来完成对应硬件的读写方式,我们就便利了许多。
-
win32程序启用控制台(控制台文件名:conout , c o n i n ,conin ,conin,conerr$)
-
大小端
大端(存储)模式,是指数据的低位保存在内存的高地址中,而数据的高位,保存在内存的低地址
中;
小端(存储)模式,是指数据的低位保存在内存的低地址中,而数据的高位,,保存在内存的高地
址中。