初识C语言 (2)
#1. 关键字
前言:C语言提供了丰富的关键字,这些关键字都是语言本身预先设定好的,用户自己是不能创造关键字的。
1.1static关键字
1.1.1 static修饰局部变量
我们来看下边一段示例代码:
void test()
{
int a = 5;
a++;
printf("%d ", a);
}
int main()
{
int i = 0;
while (i < 10)
{
test();
i++;
}
return 0;
}
这段代码会打印10个6,为什么呢?因为a这个变量是定义在test这个函数内部的,是个局部变量。每次调用test函数时,a变量会被重新创建并且被赋初始值为5,再经过++操作变成6被打印,当test函数调用完毕时,a这个局部变量又会被销毁。当下一次调用时,a又会被重新创建并被赋初始值为5,这样一直重复10次。
假若我们用static修饰a变量呢?
void test()
{
static int a = 5;
a++;
printf("%d ", a);
}
int main()
{
int i = 0;
while (i < 10)
{
test();
i++;
}
return 0;
}
此刻,就会在屏幕上打印6-15.
原因:被static修饰的局部变量,该变量的空间直到整个程序运行结束(main函数执行完毕)才会被销毁。当第一次调用test函数时,a被创建赋初始值为5,++之后变成6被打印,但是这次结束调用时,a变量的空间并不会被销毁,并且a的值为6,当第二次调用test函数时a的值仍然为6,并且a的地址不会发生变化,经过++操作之后,a变为7……以此重复10次。
注意:被static修饰的局部变量在编译阶段就被创建了。
但是为什么被static修饰之后,局部变量的空间就变得“长期有效”了呢?
解释:在c/c++中,内存空间被大致分为了三部分:
栈区:存放局部变量,形参,以及 具有临时作用的变量,栈区变量的特点是,进入作用域就创建,出了作用域就被销毁。
堆区:用于动态内存分配的(malloc,realloc,calloc)
静态区:存放全局变量,静态变量,静态区的变量的特点是:创建之后,直到程序结束才被销毁所以:局部变量本来是存放在栈区的,但是被static修饰的局部变量就被放在了静态区了,static修饰局部变量,改变了变量的存储类型,使局部变量的生命周期变长了,但是作用域不变,直到程序结束才结束。
1.1.2 static修饰局部变量
我们在一个程序中创建两个源文件:
test1.c
int g_val = 10;
test2.c
#include<stdio.h>
extern int g_val;
int main()
{
printf("%d", g_val);//打印10
return 0;
}
假如test1.c中的g_val被static修饰呢?那么程序将报错无法运行。
因为:全局变量具有外部链接属性,所以可以在其他的源文件内使用。static修饰全局变量时,改变了全局变量的链接属性,由外部链接属性变成了内部链接属性。所谓内部链接属性,就是只能在自己内部源文件能看到。这个静态变量只能在自己的源文件内部使用,不能在其他的源文件内使用了 。
1.1.3 static修饰函数
test1.c
int Add(int x, int y)
{
return x + y;
}
test2.c
extern int Add(int x, int y);
int main()
{
int a = 10;
int b = 10;
int sum = Add(a, b);
printf("%d", sum);//打印20
return 0;
}
若Add函数被static修饰,那么该函数只能在该源文件内部使用。
因为:static修饰函数,函数本身也具有外部链接属性,被static修饰之后,就变成了内部链接属性。使得函数只能在自己所在的源文件内部使用,不能在其他文件内使用。
1.2.struct关键字
当我们想要用C语言描述一个学生时,单靠C语言本身的数据类型很难做到。C语言本身提供的类型有点单一,所以这时就可以自定义一个类型,可以用struct关键字来定义。例如:
struct Student
{
char name[10];
int age;
char sex[5];
};
此时我们就创建了一个结构体类型,结构体中的name,age,sex称之为结构体成员。struct Student称为结构体类型,这个struct Student 是自定义类型,int是C语言自带的类型,他们的用法类似,都能创建变量。接下来我们利用struct Student 创建一个结构体变量。如下:
int main()
{
struct Student student = { "张山",19,"男" };
struct Student student1 = { "李四",17,"男" };
return 0;
}
此时我们创建了学生student和student1,我们可以这样理解,struct Student就相当于是个房间设计图纸,通过这个图纸我们设计出很多房间。在创建结构体的同时,我们对结构体成员进行初始化。注意:初始化时要和结构体的成员相对应,例如:张山对应char name[10]; 19对应int age;男对应char sex[5];
那么我们如何访问这些结构体成员呢?这里就要提到之前的 . (结构体成员访问符),下面我们使用这个操作符打印一下结构体成员:
struct Student
{
char name[10];
int age;
char sex[5];
};
int main()
{
struct Student student = { "张山",19,"男" };
struct Student student1 = { "李四",17,"男" };
printf("%s %d %s", student.name, student.age, student.sex);//打印结果: 张山 19 男
return 0;
}
1.3 typedef关键字
定义:typedef是类型重定义符号。可以为一个类型重命名。看示例代码:
注意:typedef是类型重定义,例如下边这段代码,被重定义之后的用法和重定义之前的用法是一样的。
unsigned long long a = 10;
int main()
{
unsigned long long b = 20;
printf("%d %d", a,b);
return 0;
}
我们在写这段代码时,会发现unsigned long long这个类型名字很长,为了方便节省时间,我们通常对其进行类型重定义操作。例如:
typedef unsigned long long ull;//被重定义之后,ull也是一个类型名
unsigned long long a = 10;//这里a和b的类型是一样的
int main()
{
ull b = 20;
printf("%d %d", a,b);
return 0;
}
改成这样就会方便很多。注意:typedef使用完之后要在语句之后加分号。(就像第一行这样,分号不能少)。
1.4 define定义常量和宏
1.4.1 define定义常量
我们看之前的文章举过的一个例子:
//编译失败 报错
int main()
{
const int a = 10;
int arr[a] = { 0 };
return 0;
}
这段代码会报错,注意:数组的括号内必须是常量,因为被const修饰的变量本质上还是变量。那么这里提到的define关键字就可以定义真正的常量。请看代码:
这里特别注意:c99标准之前,数组的大小不能是变量。但在c99标准中引入了变长数组的概念,这时允许数组的大小是变量,但是不能直接被初始化,但是visual studio编译器不支持变长数组。但是有些编译器是支持的,例如:gcc编译器就支持c99中的变长数组。
注意:使用define时,前面的#不要忘了哟
//成功运行
#define MAX 100
int main()
{
int arr[MAX] = { 0 };
return 0;
}
这里的由#define定义的就是常量。#define不仅仅能定义数字,例如:
//成功运行
#define NAME "张山" //define定义字符串
int main()
{
printf("%s", NAME);//注意以%s格式打印字符串
return 0;
}
当然还能定义其他的类型,这里就不赘述了。
1.4.2 define定义宏
回忆一下之前讲过的函数部分,我们提到了Add函数,能完成两个数的相加,下面我们就用宏来实现类似的相乘的功能。
//语法格式 #define 宏名(宏参数)宏体
#define ADD(x,y) ((x)*(y))
int main()
{
int a = 10;
int b = 20;
int sum = ADD(a+b, b);
printf("%d", sum);
return 0;
}
可以自己感受一下宏的用法,这里建议((x)*(y))这里分别给x和y的括号不要省略了,假若我们去掉,测试一下:
#define ADD(x,y) (x*y)
int main()
{
int a = 10;
int b = 20;
int sum = ADD(a+b, b);
//编译的时候会替换成: int sum = (a+b*b) 不是我们想要的运算顺序和运算结果
printf("%d", sum);
return 0;
}
我们会发现,这里就得不到我们想要的结果了,因为宏只是简单字面替换。
2 指针
2.1 引言
我们日常写的代码是会被加载到内存当中的,内存是电脑上特别重要的存储器,计算机中程序的运行都是在内存中进行的 。 所以为了有效的使用内存,就把内存划分成一个个小的内存单元,每个内存单元的大小是1个字节。 为了能够有效的访问到内存的每个单元,就给内存单元进行了编号,这些编号被称为该内存单元的地址,这些地址也称为指针。
2.2 用法
而我们想要拿到一个变量的地址,就要利用&(取地址)这个符号,得到的就是内存中的地址.例如:
int a = 10;//创建一个int类型变量
&a//取出a的地址
那么我们取出来的地址该存放在哪里呢?这时我们就需要创建一个指针变量了,指针变量用于接收变量的地址,我们看下面一段示例代码:
int a = 10;
int * pa = &a;
这里具体说明一下第2行的意思,int * pa,这里的*说明pa是一个指针变量,而前面的 int 说明 pa 这个指针变量中存放的是 a 这个 int 类型数据的地址,一般而言,这里*前面的int要与变量a的类型相对应,但是也可以不用对应,后期会讲解的。
2.3 指针变量的大小
指针变量是用来存地址的,而内存中的地址编码与电脑的内存中的真实存在的物理电位线有关,所以,==无论是什么类型的指针变量,只要物理电位线是相同的,那么指针变量在内存中占用的内存的大小也就确定了。==一般而言,32位系统有32个电位线,64位系统含有64个电位线。
在32位系统下,指针变量的大小是4个字节
在64位系统下,指针变量的大小是8个字节
这里我们就以整形指针为例做个测试:(当然其他类型的指针的结果也是类似的,自己动手去测试测试吧!)
64位:
int main() { int a = 10; int* pa = &a; printf("%d", sizeof(pa));// return 0; }
32位:
2.4 指针的简单应用
这里回忆之前的struct关键字,struct关键字可以用于创建一个结构体,我们看下面一段示例代码:
#include<stdio.h>
struct Student
{
char name[10];
int age;
char sex[5];
};
void print1(struct Student t)
{
printf("%s %d %s", t.name, t.age, t.sex);
}
int main()
{
struct Student student = { "张山",19,"男" };
print1(student);
return 0;
}
print1函数是直接接受主函数传过来的student,再通过形参t进行打印。这种方法有个缺陷,就是当结构体的成员过多时,函数的形参的拷贝量比较大,占用的空间会比较大。而之前提到,指针变量的大小往往是4/8个字节,那我们能否直接将结构体的地址传递给函数呢?我们测试一下:
struct Student
{
char name[10];
int age;
char sex[5];
};
void print2(struct Student* t)
{
printf("%s %d %s", t->name, t->age, t->sex);
}
int main()
{
struct Student student = { "张山",19,"男" };
print2(&student);
return 0;
}
这样也能成功打印结构的成员,但是注意这里代码第12行传参传递的是结构体的地址,所以在接收这个参数时,我们需要使用对应的指针变量进行接收,传递的是struct Student 类型,就需要使用struct Student* 类型接收。像这种情况,print2函数被调用时,传递的参数是地址,称之为传址调用。
注意:此处在函数内部使用的指针变量访问结构体成员,与之前的方法又略有不同。这里我们使用的是 -> 这是英文输入法下的-和>组合而成。
这里使用传址调用的好处是:形参占用的空间内存更小了,并且在内存更小的情况下我们仍能实现相同的功能。
2.5 指针的解引用操作
当我们拿到了指针的地址并且将其存放于指针变量中,我们怎样通过指针变量寻找到这个变量呢?这里就要用到解引用操作符*,看示例代码:
int main()
{
int a = 10;
printf("%d\n", a);//打印10
int* pa = &a;
*pa = 20;
printf("%d\n", a);//打印20
return 0;
}
运行这段代码我们发现,a变量的值被改变了。这里解释一下:这里指针变量pa存放的是a的地址,所以我们就可以在pa这个指针变量之前加一个*(解引用操作符)去访问a这个变量,所以这里*pa和a是等价的,前提是pa的指针的类型和a的数据类型要对应,例如:这里就是整形指针pa对应整形数据类型a。
完结:
本章的内容就到这里啦,若有不足,欢迎评论区指正,最后,希望大佬们多多三连吧,下期见!