1、C语言编译环境
1.1 notepad++
可以使用notepad++,进行C文件的编写,习惯会更接近于Linux编写。
先创建.c文件,然后放到notepad++编写即可。
在命令行使用gcc进行编译,要先离线安装mingw,在线安装很容易失败。
mingw解压之后,直接在命令行运行gcc命令是不行的,要先进到mingw64/bin这个文件夹中才行,那么将这个路径添加到(此电脑-属性-高级系统设置-环境变量-系统变量-Path-新建)之后,在打开终端之后就可以直接使用gcc命令了。
要编译的时候,使用cd命令跳到指定代码文件夹
bash
#切换目录
cd /d E: #切换到E盘了
然后进行编译和运行
bash
gcc test.c #编译test.c文件,会生成一个a.exe文件
gcc test.c -o b #把生成的文件名改成b.exe
a.exe #运行test程序产生的结果
如果代码中有输出中有中文,可以在notepad++的编码中选择编码字符集-中文-GB2312(简体中文),然后重新编译就可以解决中文乱码问题
2、指针相关
2.1 什么是指针
指针就是地址,*是取地址运算符,使用*将地址内的值读出
指针变量就是存放地址的变量
2.2 *的作用
*的两种作用:标识作用,用于定义一个指针变量(只产生在指针变量定义或声明的时候);运算作用,去使用一个指针变量。
2.3 指针偏移
指针+1,具体加多少和指针的类型有关。
指针变量可以自增,指针常量不可以。
cpp
int main()
{
int arr[3] = {1,2,3};
int *p = arr;
//这里用偏移的方式访问
for(int i = 0; i<3; i++){
printf("%d ",*(arr+i));
}
//这里p是指针变量,可以自增
for(int i = 0; i<3; i++){
printf("%d ",*p++);
}
//这里arr是指针常量,不可以自增
for(int i = 0; i<3; i++){
printf("%d ",*arr++);
}
return 0;
}
2.4 二维数组 野指针 数组指针 函数指针 指针数组 指针函数 二级指针
二维数组,本质上是个特殊的一维数组。C语言规定数组名代表数组首元素的地址,那么一个3x4的二维数组a可以表示为a[b1[4],b2[4],b3[4]],即a是有3个b的一维数组,而b是有4个元素的一维数组。a是整个二维数组的地址,a[0]可以理解为是a中的子数组名(子数组首地址),&(a+1)表示从b1到b2,&(a[o]+1)表示从b1[0]到b1[1],而*(a+1)表示的意思与&(a[o]+1)相同,因为当用指针时,操作的就是整个数组的首地址,不会区分是一维还是二维。
野指针,没有明确的内存指向,很危险。对野指针的内存空间进行操作是不被允许的。而悬空指针是一种特殊的野指针,C语言中的指针可以指向一块内存,如果这块内存稍后被操作系统回收(被释放),但是指针仍然指向这块内存,那么此时该指针就是"悬空指针"。在释放内存后,使用p=NULL,可以防止悬空指针。
cpp
char *p;//野指针
*p = 'a';
数组指针,是定义一个指针,指向一个数组。数组指针才是真正等同于二维数组名。
函数指针,
cpp
int getData(int a, int b);
int (*p)(int a, int b); //*的优先级要低于(),所以定义函数指针要把*p用()括起来
int *p(int a, int b); //表示p(int a, int b)这个函数返回的是一个地址
//使用函数指针
void printWelcome()
{
puts("程序启动,欢迎使用\n");
}
int main()
{
void (*p)();//定义一个函数指针
p = printWelcome;//指向函数
printWelcome();//使用变量名(函数名)直接调用
(*p)();//使用函数指针间接调用
return 0;
}
指针数组,强调的是数组,即一个数组,其元素均为指针类型,也就是说该数组的每一个元素都存放一个地址,相当于一个指针变量。
cpp
int * p[4];//定义一个指针数组,[]比*优先级更高,所以p先和[4]结合
int (* p)[4];//这是数组指针,指向一维数组的指针变量
指针函数,返回指针值的函数。
cpp
int *p; //指针变量,
int* p(); //变量函数,返回值是指针,返回的指针指向整型变量
二级指针(多级指针), 理解可以当作一级指针逐步进行。二级指针不能简单粗暴的指向二维数组。如果保存的地址是指针变量地址,那么就需要用二级指针来保存,而不能用一级指针。
cpp
int data = 100;
int *p = &data;
printf("data的地址:%p\n",&data);
printf("p保存data的地址:%p\n,内容是%d\n",p,*p);
int **p2;
p2 = &p;
printf("p2保存p的地址:%p\n",p2);
printf("p2是:%p\n",*p2);
printf("**p2来访问data:%d\n",**p2);
//
int main()
{
int scores[3][4]={
{55,66,77,88},
{66,55,99,100},
{11,22,33,59},
};//int (*p)[4];
int (*p2)[4] = scores;
/*
int **p;
p = scores;//会出问题!
printf("scores:%p\n",scores);
printf("p=%p\n",p);
printf("*p=%p\n",*p);//*p是一个野指针,不是我们认为的会变成列地址
printf("*score=%p\n",*scores);//*p是一个野指针,不是我们认为的会变成列地址
**p = 100;
printf("done\n");*/
int **p3 = &p2;//能用
**p3 = 100;//能改的动,但是一般不这么进行
printf("%d\n",scores[0][0]);
return 0;
}
2.5 指针练习题
cpp
1、一个整形数:
2、一个指向整形数的指针:
3、一个指向指针的指针,它指向的指针指向一个整形数:
4、一个有10个整形数的数组:
5、一个有10个指针的数组,每个指针指向一个整形数:
6、一个指向有10个整形数的数组的指针:
7、一个指向指针的指针,被指向的指针指向一个有10个整形数的数组:
8、一个指向数组的指针,该数组有10个整形指针:
9、一个指向函数的指针,该函数有一个整形参数并返回一个整形数:
10、一个有10个指针的数组,每个指针指向一个函数,该函数有一个整形参数并返回一个整形数:
11、一个函数的指针,指向的函数的类型是有两个整形参数并且返回一个函数指针的函数,返回的函教指针指向有一个整形参数且返回整形数的函数:
1、int a;
2、int *a;
3、int **a;
4、int a[10];
5、int *a[10];
6、int (*a)[10];
7、int (**a)[10];
8、int *(*a)[10];
9、int (*a)(int);
10、int (*a[10])(int);
11、int (*(*a)(int,int))(int);
3、字符串相关
3.1 定义字符串
cpp
char data[] = "hello";//使用数组的方式定义,这是变量
char *pchar = "hello";//使用指针定义,这是字符串常量,不允许被修改
data[3] = 'm';//允许
*pchar = 'm';//不允许
3.2 字符串和字符数组的区别
cpp
char cdata[] = {'h','e','l','l','o'};//这不是一个标准的字符串概念,只是用一个数组的方式表现出来
char cdata2[] = "hello";
int len = sizeof(cdata)/sizeof(cdata[0]);
printf("len= %d\n",len);//len=5
int len = sizeof(cdata2)/sizeof(cdata2[0]);
printf("len= %d\n",len);//len=6,因为字符串默认还有一个字符串结束标志'\0'
3.3 sizeof和strlen的区别
sizeof是计算分配区域的大小,strlen是计算字符串的有效长度。
cpp
#include <stdio.h>
#include <string.h>
int main()
{
char cdata[] = "hello";
printf("sizeof: %d\n", sizeof(cdata)); //len=6
printf("strlen: %d\n", strlen(cdata)); //len=5
char *p = "hello";
printf("sizeof: %d\n", sizeof(p)); //len=8,p是一个char *型的指针,用sizeof计算得出的是计算机用多少字节来表示一个地址
printf("strlen: %d\n", strlen(p)); //len=5
return 0;
}
3.4 动态开辟字符串
malloc,函数原型是void *malloc(size_t size),代表由C库函数void *malloc(size_t size)分配所需的内存空间,并返回一个指向它的指针。使用这个函数需要在头文件包含stdlib.h这个库。
realloc,函数原型是void *realloc(void *ptr,size_t size),用于扩容,代表C库函数void *realloc(void *ptr,size_t size)尝试重新调整之前调用malloc或者calloc所分配的ptr所指向的内存块的大小。
free,如果用malloc、calloc、realloc等开辟了内存,那么在使用完之后要通过free释放内存 ,防止内存泄漏。函数原型是void free(void *ptr)。
cpp
void *p = malloc(size);
free(p);
// 避免"悬空指针"
p = NULL;
memset,函数原型是void *memset(void *str, int c, size_t n),代表复制字符 c(一个无符号字符)到参数 str 所指向的字符串的前 n 个字符。可以对较大的结构体或数组进行快速清零操作。
3.5 字符串常用api
输出字符串:puts()、printf("%s\n",p);
获取字符串:scanf("%s", str)、gets(str);但是因为gets函数可以无限读取,易发生溢出。如果溢出,多出来的字符将被写入到堆栈中,这就覆盖了堆栈原先的内容,会破坏一个或多个不相关变量的值。
计算字符串长度strlen,函数原型是size_t strlen(const char *str) 计算字符串 str 的长度,直到空结束字符,但不包括空结束字符。
拷贝字符串:strcpy,函数原型是char *strcpy(char *dest, const char *src) 把 src 所指向的字符串复制到dest。strncpy,函数原型是char *strncpy(char *dest, const char *src, size_t n) ,把src 所指向的字符串复制到dest,最多复制 n 个字符。当 src 的长度小于 n 时,dest 的剩余部分将用空字节填充。
字符串拼接:strcat,函数原型是 char *strcat(char *dest, const char *src) 把 src 所指向的字符串追加到 dest 所指向的字符串的结尾。要保证dest的空间足够
字符串比较:strcmp,函数原型是 int strcmp(const char *str1, const char *str2) 把 str1 所指向的字符串和 str2 所指向的字符串进行比较。如果 str1 小于 str2,那么返回值小于 0;如果 str1 等于 str2,那么返回值等于 0;如果 str1 大于 str2,那么返回值大于 0。
strncmp,函数原型是 int strncmp(const char *str1, const char *str2, size_t n) 把 str1 和 str2 进行比较,最多比较前 n 个字符。返回值于strcmp相同。
查找子字符:strchr,函数原型是 char *strchr(const char *str, int c) ,在参数 str 所指向的字符串中搜索第一次出现字符 c(一个无符号字符)的位置。如果在字符串 str 中找到字符 c,则函数返回指向该字符的指针,如果未找到该字符则返回 NULL。
查找子串:strstr,函数原型是 char *strstr(const char *haystack, const char *needle)在字符串 haystack 中查找第一次出现字符串 needle 的位置,不包含终止符 '\0'。该函数返回在 haystack 中第一次出现 needle 字符串的位置,如果未找到则返回 null。
字符串分割:strtok,函数原型是 char *strtok(char *str, const char *delim) 分解字符串 str 为一组字符串,delim 为分隔符。该函数返回被分解的第一个子字符串,如果没有可检索的字符串,则返回一个空指针。特别要注意的是分割处理后原字符串会str会改变,原字符串的改动是切分符原位置均更改为'\0'。
断言:assert,函数原型是voidassert(int expression) ,允许诊断信息被写入到标准错误文件中。换句话说,它可用于在 C 程序中添加诊断。expression -- 这可以是一个变量或任何 C 表达式。如果 expression 为 TRUE,assert() 不执行任何动作。如果 expression 为 FALSE,assert() 会在标准错误 stderr 上显示错误消息,并中止程序执行。
cpp
#include <stdio.h>
int main()
{
char *p = "hello";
char str[128] = {'\0'};
//输出字符串
puts(p); //使用puts输出,自带换行符
printf("%s\n",p);
//获取字符串
scanf("%s", str);
puts(str);
gets(str);
puts(str);
return 0;
}
int main ()
{
const char str[] = "https://www.runoob.com";
const char ch = 'o';
char *ptr;
ptr = strchr(str, ch);
if (ptr != NULL) {
printf("字符 'o' 出现的位置为 %ld。\n", ptr - str + 1); //字符 'o' 出现的位置为 16
printf("|%c| 之后的字符串是 - |%s|\n", ch, ptr); //|o| 之后的字符串是 - |oob.com|
} else {
printf("没有找到字符 'o' 。\n");
}
return(0);
}
int main()
{
const char haystack[20] = "RUNOOB";
const char needle[10] = "NOOB";
char *ret;
ret = strstr(haystack, needle);
printf("子字符串是: %s\n", ret); //子字符串是: NOOB
return(0);
}
int main () {
char str[80] = "This is - www.runoob.com - website";
const char s[2] = "-";
char *token;
/* 获取第一个子字符串 */
token = strtok(str, s);
/* 继续获取其他的子字符串 */
while( token != NULL ) {
printf( "%s\n", token ); //www.runoob.com ;website
token = strtok(NULL, s);
}
return(0);
}
4、结构体
4.1 结构体的定义
如果需要将不同元素类型的数据集合在一起,那么就需要用到结构体了。
cpp
//结构体模版,一般不给赋具体的值,每一项在实际应用中并不是都要使用
//每个成员都是结构体中的一个域,成员列表,也成为域表
struct Student //struct告知系统是一个结构体
{
int num;
char name[32];
char sex;
int age;
double score;
char addr[32];
}; //分号不能忘
struct Student stu1; //结构体变量声明
struct Student
{
int num;
char name[32];
char sex;
int age;
double score;
char addr[32];
} stu1,stu2; //在声明结构体的同时声明了两个结构体变量,实际当中最好不要这样用
4.2 结构体的使用
使用 . 来访问结构体中的成员。
4.3 结构体数组
cpp
struct Student //struct告知系统是一个结构体
{
int num;
char name[32];
char sex;
int age;
double score;
char addr[32];
};
int main()
{
struct Student arr2[3] = {
{2, "张三", 'm', 17, 99,5, "北京"},
{3, "李四", 'm', 18, 97,5, "上海"},
{4, "小红", 'g', 17, 99,9, "广州"},
};
len = sizeof(arr2)/sizeof(arr2[0]);
for(i = 0; i < len; i++){
printf("学号:%d, 姓名:%s, 年龄: %d,分数:%lf,地址:%s\n ",
arr2[i].num, arr2[i].name, arr2[i].age, arr2[i].score, arr2[i].addr);
}
return 0;
}
4.4 结构体指针
使用结构体指针时,要把以前的普通变量名,或者下标访问的 . 运算符,改成结构体指针的->;指针++之后,每次遍历会到数组尾巴,下次遍历之前记得要回来(重新指向数组头)。
cpp
#include <stdio.h>
struct Test
{
int idata;
char cdata;
};
int main()
{
int a = 10;
int *p = &a;
char c = 'c';
char *pc = &c;
struct Test t1 = {10, 'c'};
struct Test *ps = &t1;
printf("t1的idata=%d\n", t1.idata);//变量名访问,用.运算符
printf("t1的idata=%d\n", ps->idata);//指针变量名访问,用->运算符
return 0;
}
5、共用体(联合体)
5.1 共用体的定义
可以存储不同的数据类型,与结构体不同的是,这些不同的数据类型共用一个存储空间,空间大小由最大类型确定,且任何时候都只能由1个成员带值。
cpp
//共用体模版,一般不给赋具体的值,每一项在实际应用中并不是都要使用
//每个成员都是共用体中的一个域,成员列表,也成为域表
union Student //union 告知系统是一个共用体
{
int num;
char name[32];
char sex;
int age;
double score;
char addr[32];
}; //分号不能忘
union Student stu1; //共用体变量声明
union Student
{
int num;
char name[32];
char sex;
int age;
double score;
char addr[32];
} stu1,stu2; //在声明共用体的同时声明了两个共用体变量,实际当中最好不要这样用
5.2 共用体的使用
访问共用体成员与结构体类似。
cpp
#include <stdio.h>
struct Person
{
char name[32];
int age;
char zhiYe;
char addr[];
union D{
int class;
char keMu[12];
}mes;
};
int main()
{
struct Person p[2];
int i;
for(i = 0; i < 2; i++){
printf("请输入职业:t代表老师,s代表学生\n");
scanf("%s", &(p[i].zhiYe));
if(p[i].zhiYe == 'c'){
printf("请输入学生班级:\n");
scanf("%d", &(p[i].mes.class));
}
else{
printf("请输入老师的科目:\n");
scanf("%s", p[i].mes.keMu);
}
getchar(); //处理输入数据之后的回车被scanf("%s", &(p[i].zhiYe));吸收的问题
}
return 0;
}
6、枚举
6.1 枚举的定义
用于定义一组具有离散值的常量,它可以让数据更简洁,更易读。枚举类型通常用于为程序中的一组相关的常量取名字,以便于程序的可读性和维护性。
cpp
//对一周七天的定义
#define MON 1
#define TUE 2
#define WED 3
#define THU 4
#define FRI 5
#define SAT 6
#define SUN 7
//使用枚举可以将上面改写为
//声明枚举类型
enum Weekday
{
MON=1, TUE, WED, THU, FRI, SAT, SUN
}; //第一个枚举成员的默认值为整型的 0,后续枚举成员的值在前一个成员上加 1。在这个实例中把第一个枚举成员的值定义为 1,第二个就为 2,以此类推。
//定义枚举变量方法一:先定义枚举类型,再定义枚举变量
enum Weekday day;
//定义枚举变量方法二:定义枚举类型的同时定义枚举变量
enum Weekday
{
MON=1, TUE, WED, THU, FRI, SAT, SUN
} day;
//定义枚举变量方法三:省略枚举名称,直接定义枚举变量
enum
{
MON=1, TUE, WED, THU, FRI, SAT, SUN
} day;
6.2 枚举的使用
不同的枚举当中,变量名不能相同。
在C 语言中,枚举类型是被当做 int 或者 unsigned int 类型来处理的。
当枚举类型连续时,可以遍历枚举。不连续时,则不行。
获取枚举中某个值,可以直接通过变量名访问。
cpp
#include <stdio.h>
enum DAY
{
MON=1, TUE, WED, THU, FRI, SAT, SUN
};
int main()
{
enum DAY day;
day = WED;
printf("%d",day);
// 遍历枚举元素
for (day = MON; day <= SUN; day++) {
printf("枚举元素:%d \n", day);
}
return 0;
}
//以下枚举类型不连续,无法完成遍历
enum
{
ENUM_0,
ENUM_10 = 10,
ENUM_11
};
7、typedef
7.1 typedef的定义
可以使用它来为已有类型取一个新的名字。
cpp
typedef unsigned char BYTE;
8、注意事项
在形参中,不存在数组的概念,即便中括号中约定了数组的大小,也无效。
cpp
void printf(int arry[10])//形参中不存在数组的概念,即使中括号中约定了数组的大小,也无效
//这里传递的是一个地址,是数组的首地址
{
int i;
printf("printArr:arry的大小是:%d\n",sizeof(array));//这里用8个字节来表示一个地址
for(i=0;i<10;i++)
{
printf("%d ",arry[i]);
}
putchar('\n');
}