C语言基础

1.指针

1. 指针指针变量

指针:代表一个内存地址

指针变量:存放指针的变量,指针变量的类型是存放的地址指向的变量的数据类型

变量名:编译器会将变量名存放到一个符号表中,每个符号对应一个地址。当调用变量时,按照符号找到对应的地址。然后进行操作。

指针和指针变量

  • 取地址运算符& 和**取值运算符 ***
1
2
char *pa = &a; //指针变量pa指向a,pa中存放的地址为a的地址
printf("%c",*pa); //取值

避免访问未初始化的指针:可能会覆盖其他的数据

2. 指针和数组

数组名是数组中第一个元素的地址

指向数组的指针:将数组的首地址存放到指针变量中

1
2
3
int a[5] = {1,2,3,4,5};
char *p = &a[0];
char *p = a;

指针的运算:指针指向数组后,可以进行加减运算。相当于在数组中向前后移动n个元素。

1
2
3
4
int a[5] = {1,2,3,4,5};
int *p = a;
printf("%d\n",*p);
printf("%d\n",*(p+1)); //指向下一个元素

使用指针定义和访问数组:

1
2
3
4
5
6
7
char *str = "Hello world"; //str指向数组的第一个元素
int i, length;
length = strlen(str);
for (i = 0; i < length; i++)
{
printf("%c",str[i]);
}

指针和数组名的区别

数组名是地址常量,而指针变量是变量,可以作为左值(lvalue)。而数组名不可以作为左值

1
2
3
4
5
6
7
char str[] = "Hello world";
char *target = str; //数组名是地址常量,不可以作为左值
int count = 0;
while (*target++ != '\0'){
count++;
}
printf("%d\n",count);

指针数组数组指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int *p1[5]; //指针数组,每个数组元素存放一个指针变量,初始化为多个指针
char *p[2] = {
"Hello",
"World"
};
printf("%c",*(p[0]+1));
int (*p2)[5]; //数组指针,指向一个数组
/* 使用指针指向数组时,下面的指针p实际上指向的是一个整型变量(也就是数组的第一个元素),
而不是指向这个数组 */
int a[5] = {1,2,3,4,5};
int *p = a;

// 定义数组指针,存放数组首地址的地址,相当于二级指针
int a[5] = {1,2,3,4,5};
int (*p2)[5] = &a; //取出数组的地址
printf("%d\n",*(*p2+1));

二维数组

1
2
3
4
5
6
int array[4][5] = {0};

/*
*(array+i) == array[i] //相当于多级指针,需要多次解引用
*(*(array+i)+j) == array[i][j]
*/

二维数组数组指针

1
2
3
4
int array[2][3] = {{1,2,3},{4,5,6}};
int (*p)[3] = array;
printf("%d\n",**(p+1));
printf("%d\n",**(array+1));

3. void指针和NULL指针

void是不确定类型,不可以用来申明变量

void指针为通用指针,可以指向任意类型的数据。void指针不能解引用,编译器不能确定指向的数据类型,需要强制转换。

1
2
3
4
5
int num = 1;
int *p = &num;
void *a;
a = p;
printf("%d\n",*(int *)a); //强制类型转换

NULL指针,不指向任何的数据的空指针。在指针不知道初始化为什么地址时,可以初始化为NULL。NULL用于指针和对象,指向一个不被使用的地址。’\0’表示字符串的结尾。

1
2
3
4
int *p1;
int *p2 = NULL;
printf("%d\n",*p1); //野指针,解引用后可能为任意值
printf("%d\n",*p2); //无法对NULL进行解引用

4.指向指针的指针

1
2
3
int num = 1';
int *p = &num;
int **pp = &p; //指向指针的指针,需要进行两层解引用

指针数组和指向指针的指针

1
2
3
4
5
6
char *p[2] = {"abc","def"};
char **pp1;
pp1 = &p[1]; //pp1指向指针p[1]
char **pp2[2]; //指向指针的指针的数组
pp2[0] = &p[0];
pp2[1] = &p[1];

常量和指针

1
2
const int num = 1; //变量num只读不可以修改
const int *p = &num; //指针指向常量,不可以通过解引用修改指向的值,但是指针的指向可以改变

常量指针

1
2
3
4
5
6
7
// 指针指向变量,指针本身不可以修改指向,但是指针指向的值可以修改
int num = 100;
int * const p = &num;
*p = 1024;
// 指针指向常量,指针本身不可以修改指向,指针指向的值也不可以修改
const int * const p = &cnum; //指向常量的常量指针
const int * const *pp = &p; //指向 指向常量的常量指针 的指针

2.函数

函数的声明与定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
int sum(int); //声明
//定义
int sum(int n){
int res = 0;
do{
res += n;
}while(n-- >0);
return res;
}

// main函数
int main(){
int n;
scanf("%d",&n);
printf("%d\n",sum(n));
return 0;
}

传值传址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 错误的示例,作用域不同导致在main中x和y的值没有互换
#include <stdio.h>
void swap(int, int); //传入变量的副本
void swap(int x, int y){
int temp;
temp = x;
x = y;
y = temp;
}

int main(){
int x = 3, y = 5;
swap(x,y);
printf("%d %d",x,y);
return 0;
}

// 传址,传给函数的是变量的地址
#include <stdio.h>
void swap(int *, int *);
void swap(int *x, int *y){
int temp;
temp = *x;
*x = *y;
*y = temp;
}

int main(){
int x = 3, y = 5;
swap(&x,&y);
printf("%d %d",x,y);
return 0;
}
// 参数为数组时,实际上传递给函数的是第一个元素的地址,也就是传入的是指针

可变参数 variable argument

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>
#include <stdarg.h>
int sum(int n,...); //n表示可变参数数量
int sum(int n,...){
int i, sum = 0;
va_list vap; //定义参数列表
va_start(vap,n); // 初始化参数列表
for (i=0;i<n;i++){
sum += va_arg(vap, int); //获取参数值
}
va_end(vap); //关闭参数列表
return sum;
}

int main(){
int x = 3, y = 5, z = 10;
printf("%d",sum(3,x,y,z));
return 0;
}

指针函数函数指针

指针函数:使用指针变量作为函数的返回值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>
char *getWord(char); //返回一个指针
char *getWord(char c){
switch(c)
{
case 'A': return "apple";
case 'B': return "banana";
default: return "None";
}
}
int main(){
char input;
scanf("%c",&input);
printf("%s\n",getWord(input)); //返回字符串首字母的地址
return 0;
}
// 不要返回局部变量的指针,局部变量只存在于函数中,函数结束即销毁

函数指针:指向函数的指针。在程序中定义了一个函数,那么在编译时系统就会为这个函数代码分配一段存储空间,这段存储空间的首地址称为这个函数的地址。而且函数名表示的就是这个地址。既然是地址我们就可以定义一个指针变量来存放,这个指针变量就叫作函数指针变量,简称函数指针。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>
int sqt(int);
int sqt(int num){
return num * num;
}

int main(){
int num;
int (*fp)(int);
scanf("%d",&num);
fp = sqt;
printf("%d",(*fp)(num));
// printf("%d",fp(num)); 这样写也可以
return 0;
}

函数指针作为参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>
int add(int,int);
int sub(int,int);
int calc(int (*fp)(int, int),int,int);
int add(int a, int b){
return a + b;
}

int sub(int a, int b){
return a -b;
}
// 函数名即为函数指针
int calc(int (*fp)(int, int), int a, int b){
return (*fp)(a,b);
}

int main(){
printf("%d\n",calc(add,3,5));
return 0;
}

函数指针作为返回值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <stdio.h>
int add(int,int);
int sub(int,int);
int calc(int (*fp)(int, int),int,int);
int (*select(char op))(int, int); //函数指针作为返回值的函数声明
int add(int a, int b){
return a + b;
}

int sub(int a, int b){
return a -b;
}

int calc(int (*fp)(int, int), int a, int b){
return (*fp)(a,b);
}

int (*select(char op))(int, int){
switch(op)
{
case '+': return add;
case '-': return sub;
}
}

int main(){
int a,b;
char op;
int (*fp)(int, int);
scanf("%d%c%d",&a,&op,&b);
fp = select(op);
printf("%d\n",calc(fp,a,b));
return 0;
}

3. C语言细节

局部变量全局变量:函数内部定义的函数是局部变量,外部定义的是全局变量

如果在函数内部存在与全局变量同名的局部变量,则在函数中屏蔽全局变量

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
void func();
void func(){
extern int count; //告知编译器count变量在后面定义
count++;
}
int count = 0;
int main(){
func();
printf("%d\n",count);
return 0;
}

使用大量的全局变量会占用更多的内存,会污染命名空间

作用域

  • 代码块作用域:形参和代码块中定义的变量只在代码块内部起作用
  • 文件作用域:从声明开始到文件结束均可以使用
  • 原型作用域:只适用于在函数原型中声明的参数名
  • 函数作用域:适用于goto语句的标签

链接属性:static可以将external的变量表示为internal变量,显示某个变量只在本文件中被使用

  • external:多个文件中同名标识符表示一个实体
  • internal:单个文件中同名标识符表示一个实体
  • none:同名标识符表示不同实体

生存期

  • 静态存储期:文件作用域的变量具有静态存储期,程序关闭时释放

  • 自动存储期:代码块作用域的变量具有自动存储期,代码块结束时释放存储空间

变量的存储类型:指存储变量值的内存类型

  • auto 自动变量

默认存储类型,自动变量具有代码块作用域,自动存储期,空链接属性。默认可以不写。

  • register 寄存器变量

寄存器存在于cpu的内部,cpu对寄存器中数据的读取和存储几乎没有任何延迟。与自动变量类似。无法获取寄存器变量的地址。

  • static静态局部变量

static声明的变量具有静态存储期,生存周期和全局变量一样,程序结束时释放。作用域仍然为代码块作用域。

  • extern

变量在其他文件中已经定义过

  • typedef :为数据类型定义别名,在结构体中使用
1
2
3
4
5
6
7
8
9
10
11
12
typedef int interger;
// 相比于宏定义的直接替换,typedef是对类型的封装
typedef struct Date
{
int year;
int month;
} DATE, *PDATE;
// 给结构体取别名为DATE和指针PDATE
// typedef简化复杂的类型声明
typedef int (*PTR_TO_ARRAY)[3]; //将PTR_TO_ARRAY定义为整型数组指针
typedef int (*PTR_TO_FUN)(void); //函数指针
typedef int *(*PTR_TO_FUN)(int); //指针数组,数组元素为函数指针

动态内存管理

  • malloc:申请动态内存空间,位于内存的堆上,需要主动释放内存。

对内存空间进行操作:memset, memcpy, memmove, memcmp, memchr

1
2
3
4
5
6
7
8
9
10
11
// void *malloc(size_t size);
// 向系统申请分配size个字节的内存,并返回一个指向这个空间的void指针
#include <stdio.h>
#include <stdlib.h>
int main(void){
int *ptr;
ptr = malloc(sizeof(int));
printf("%p\n",ptr);
free(ptr);
return 0;
}
  • free:释放动态内存空间
1
2
// void free(void *ptr);
// 释放ptr指向的内存空间

内存泄漏:申请的动态内存空间应该及时释放,否则会导致内存不足。或者丢失申请内存块的地址。

  • calloc:申请并初始化一系列内存空间
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void){
int *ptr = NULL;
int i;
ptr = (int *)malloc(10 * sizeof(int));
memset(ptr, 0, 10 *sizeof(int)); //使用memset进行初始化内存空间为0
for (i=0; i < 10; i++){
printf("%d ",ptr[i]);
}
putchar('\n');
free(ptr);
return 0;
}
// calloc分配内存空间并初始化
int *ptr = (int *)calloc(8, sizeof(int));
// malloc分配内存空间,使用memset进行初始化
int *ptr = (int *)malloc(8 * sizeof(int));
memset(ptr, 0, 8 * sizeof(int));
  • realloc:重新分配内存空间
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
void *realloc(void *ptr, size_t size);
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void){
int *ptr1 = NULL;
int *ptr2 = NULL;
ptr1 = (int *)malloc(10 * sizeof(int));
ptr2 = (int *)malloc(20 * sizeof(int));
memcpy(ptr2, ptr1, 10);
free(ptr1);
free(ptr2);
return 0;
}
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void){
int i, num;
int count = 0;
int *ptr = NULL;
do
{
scanf("%d",&num);
count++;
ptr = (int *)realloc(ptr,count * sizeof(int));
if (ptr == NULL){
exit(1);
}
ptr[count-1] = num;
} while (num != -1);
for(i = 0; i < count; i++){
printf("%d\n",ptr[i]);
}
free(ptr);
return 0;
}

内存管理

malloc分配内存会导致大量内存碎片的产生,同时有时间上的消耗

内存池:程序额外维护的一个缓存区域。用户申请内存块的时候,优先在内存池中查找合适的空间。释放内存时,优先释放到内存池中。

c语言的内存布局规律

内存地址从低至高排列:

  • 代码段:存放程序执行代码的一部分,如函数
  • 数据段:存放已初始化的全局变量和局部静态变量
  • BSS段:存放未初始化的全局变量和局部静态变量
  • 堆:存放动态分配的内存段,手动申请。堆中的数据可以由不同的函数访问到。从低地址向高地址发展。
  • 栈:存放局部变量,参数和返回值等,系统自动分配。由高地址向低地址发展。

c语言的预处理(#和##)

  • 文件包含
  • 宏定义
  • 条件编译
1
2
3
4
5
6
7
8
9
10
11
// 不带参数的宏定义,变量大写
// 编译器不会对宏定义进行语法检查
#define PI 3.14
#define S PI * 25
#undef //终止宏定义
// 带参数的宏定义,括号的存在是确保运算符优先级不同导致的错误
#define max(x,y) (((x) > (y))?(x) : (y))
#define STR(s) # s // #将实参s当做字符串处理
// ##运算符叫做记号连接运算符,可以连接两个参数
#define TEST(x,y) x ## y
// 输入2和1,输出21

内联函数:解决程序中函数调用的效率问题。减少了函数调用的时间消耗,但是增加了代码编译的时间。

4. 结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 将不同的数据类型集合到一起
// 声明结构体
struct Book
{
char title[128];
char author[40];
float price;
unsigned int date;
char publisher[40];
};
//定义结构体类型变量
struct Book book;
//初始化结构体
struct Book book = {
.price = 10.8,
.date = 20170912
};
//结构体的内存对齐,按四个字节进行对齐。按照定义结构体成员的位置可以减小结构的内存大小
// 结构体嵌套
struct Date
{
int year;
int month;
int day;
};
struct Book
{
char title[128];
float price;
struct Date date;
};

结构体数组:数组元素为结构体,而不是基本的数据结构

1
2
3
4
5
6
7
8
// 定义结构体数组
struct Date
{
int year;
int month;
int day;
} date[10]; //定义结构体数组
struct Date date[10];

结构体指针

1
2
3
4
5
6
struct date *pt; //定义指向结构体的指针
// 结构体的变量名不是指向结构体的地址
pt = &date; //使用取址运算符
// 访问成员
(*pt).year;
pt->year; //成员选择

传递结构体变量

1
2
// 结构体作为输入和输出
struct 结构体1 函数名( struct 结构体2 结构体2变量);

传递指向结构体变量的指针

1
2
// 指向结构体变量的指针 作为输入和输出
struct 结构体1 *函数名( struct 结构体2 *结构体2变量);

动态申请结构体

1
2
struct Book *b1;
b1 = (struct Book *)malloc(sizeof(struct Book));

单链表

1
2
3
4
5
6
7
// 使用结构体定义单链表
struct Test
{
int x;
int y;
struct Test *next;
};

5. 共用体

共用体,或者称为联合体或联合类型。共用体的所有成员同享一个内存地址,只能使用某一个成员。否则成员的值之间会互相覆盖。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>
#include <string.h>
union Test
{
int i;
double pi;
char str[6];
};

int main(void){
union Test test; //定义共用体类型变量
test.i = 10;
test.pi = 1.1;
strcpy(test.str, "Hello"); //值为最后赋值的成员值
printf("%p\n",&test.i); // 三者的地址相同
printf("%p\n",&test.pi);
printf("%p\n",&test.str);
return 0;
}

6. 枚举类型

1
2
enum Week {sun, mom, tue, wed, thu, fri, sat}; //枚举常量,可以指定枚举常量的值
enum Week today; //枚举变量

(1) 枚举型是一个集合,集合中的元素(枚举成员)是一些命名的整型常量,元素之间用逗号,隔开。

(2) Week是一个标识符,可以看成这个集合的名字,是一个可选项,即是可有可无的项。

(3) 第一个枚举成员的默认值为整型的0,后续枚举成员的值在前一个成员上加1。

(4) 可以人为设定枚举成员的值,从而自定义某个范围内的整数。

(5) 枚举型是预处理指令#define的替代。

(6) 类型定义以分号**;**结束。

7. 位

位域:能够将一个字节拆分使用,位域是字节的一部分

1
2
3
4
5
6
7
struct Test
{
unsigned int a:1; //指定变量所占的位数
unsigned int b:1;
unsigned int c:5;
unsigned int :10; //无名位域,凑空间,不可以使用
};

逻辑位运算符:对位进行运算。用于掩码,打开位,关闭位,转置位等

1
2
3
4
5
6
/*
~ 按位取反
& 按位与
^ 按位异或(XOR)
| 按位或
*/

移位运算符:将变量的二进制位进行左移或右移

1
2
3
4
5
6
7
8
9
10
11
12
/* 左移位运算符,右端抛弃,左端用0填充。右移运算符同理
11001010 << 2
00101000
*/
int main(void){
int value = 1;
while (value < 1024){
value <<= 1;
printf("%d\n",value);
}
return 0;
}

8. 文件操作

  • 文本文件二进制文件

  • 打开和关闭文件

1
2
fopen
fclose
  • 文件读写
1
2
3
4
5
6
7
8
9
10
11
//读写单个字符
fgetc //函数
getc //宏
fputc //函数
putc //宏
//读写字符串
fgets
fputs
// 格式化读写文件
fscanf //从文件中获取
fprintf //输入至文件中
  • 二进制读写文件

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!