C语言_变量_常变量_作用域_static

作用域

局部变量

1
2
3
4
5
6
7
8
9
10
11
12
13
#include<stdio.h>
void bar();
int main()
{
bar();
bar();
return 0;
}
void bar()
{
int val = 10;
printf("%i\n", val++);
}

以上程序两次都打印10。因为val的作用域只生存在函数当时所分配的栈帧中,此变量为自动/局部变量(auto/local variable),函数调用结束后变量自动销毁。

全局变量

全局变量(global variable)。

1
2
3
4
5
6
7
8
9
10
11
12
13
#include<stdio.h>
void var();
int val = 10;
int main()
{
bar();
bar();
return 0;
}
void bar()
{
printf("%i\n", val++);
}

第一次打印10,第二次打印11。

多文件下使用全局变量 - extern

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//文件A
#include<stdio.h>
void var();
//int val = 10;
extern int val;
int main()
{
bar();
bar();
return 0;
}
void bar()
{
printf("%i\n", val++);
}
//文件B
int val = 10;
  1. 只需要声明,不用赋值。
  2. extern声明之后不会再次分配额外的空间
  3. extern在编译时不检查。
  4. extern在链接时会检查其他文件中实际有没有这个变量。

更优雅的全局变量 - 静态变量

static修饰变量和函数的作用

  • static修饰变量: 在函数内部使用static修饰变量时,该变量称为静态局部变量。它的生命周期延长到整个程序运行期间,但作用域仅限于定义它的函数内部。静态局部变量在每次函数调用时不会被重新初始化,而是保留上一次调用结束时的值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>

void exampleFunction()
{
// 静态局部变量
static int counter = 0;
counter++;
printf("Counter: %d\n", counter);
}

int main()
{
exampleFunction(); // 输出:Counter: 1
exampleFunction(); // 输出:Counter: 2
exampleFunction(); // 输出:Counter: 3

return 0;
}

在这个例子中,静态局部变量counter在每次函数调用之间保留其值,而不会被重新初始化。

  • static修饰函数: 在函数声明或定义时使用static修饰,将函数的链接属性变为内部链接,限制其作用域在当前源文件内。这样,其他源文件无法调用这个静态函数。
1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>

// 静态函数声明,限制其作用域在当前源文件内
static void staticFunction()
{
printf("This is a static function.\n");
}
int main()
{
staticFunction(); // 可以调用静态函数
return 0;
}

在这个例子中,使用static修饰的函数staticFunction只能在当前源文件内被调用。

static声明语句什么时候执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include<stdio.h>
void var();

int main()
{
bar();
bar();
return 0;
}
void bar()
{
static int val = 10;
printf("%i\n", val++);
}

之前我一直以为static在函数第一次调用时会被执行,其他时候函数调用则跳过。经过调试,发现,static语句在第一次调用函数时也会直接跳过这条语句。这说明static变量在main函数执行前就被初始化了。

static的本质

  1. 本质上还是一个全局变量
  2. 在应用程序建立的时候,静态变量就创建好了。
  3. 在程序中写的意义只是说明变量名字的可见范围。如果写在大括号(不仅是函数体大括号,所有大括号都算)里,则出了大括号后就不认识该标识符;如果写在main函数外,则可见范围仅限于此文件,所有函数都可见。
    1. 如果函数里、函数外定义了同名static变量则:就近原则,优先选择本函数里的!
1
2
3
4
5
6
7
8
9
#include<stdio.h>
void fun()
{
static int val = 1;
}
int main()
{
printf("val: %i\n", val); // error, can't find val
}
1
2
3
4
5
6
7
8
9
10
11
#include<stdio.h>
static int val = 34;
void fun()
{
static int val = 1;
}
int main()
{
fun();
printf("val: %i\n", val); // 函数外的val,不认识fun函数里的val
}
1
2
3
4
5
6
7
8
9
10
11
12
13
#include<stdio.h>
static int val = 34;
void fun()
{
static int val = 1;
val = 2; // 优先选择本函数里的val
}
int main()
{
fun();
printf("val: %i\n", val); // 打印34。读取的是函数外的val,不认识fun函数里的val
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include<stdio.h>
static int val = 34;
void fun()
{
{
static int val = 1;
}
val = 2; // 只认识函数外的val,函数里的上面这个大括号里的val不认识
}
int main()
{
fun();
printf("val: %i\n", val); // 函数外的val,不认识fun函数里的val
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include<stdio.h>
static int val = 34;
void fun()
{
static int val = 1;
val = 2; // 函数外的val
}
int main()
{
static int val = 33;
fun();
printf("val: %i\n", val); // 打印33,优先选择本函数里的val
}

1
2
3
4
5
6
7
#include<stdio.h>
#define PI 3.14
int main()
{
int i = PI;
return 0;
}

常变量

如果PI赋予了普通的int型变量i,那么i可能会被篡改。这时可以用常变量修饰i

1
2
3
4
5
6
7
8
9
#include<stdio.h>
#define PI 3.14
int main()
{
//int i = PI;
//i = 9.78;
const int i = PI; // or: int const i = PI;
return 0;
}

但是常变量不建议这么使用。而是如下使用:

1
2
3
4
5
6
7
#include<stdio.h>
const float PI = 3.14f;
int main()
{
float i = PI;
return 0;
}

好处:

  1. 宏能做的,常变量都可以做到。
  2. 宏不能做到的:变量类型检查,常变量可以做。
    1. 虽然3.14f可以正确地表达float字面常量
    2. 但是90却不能正确地表达short字面常量,char、short诸如此类都是整型兼容的,形式与整型无区别,所以无法在宏定义时把类型信息附加上,因此丢失了类型信息。

其他总结

  1. #define宏定义和const的区别:
    • #define宏定义: 使用#define创建的宏是一种简单的文本替换机制。在预处理阶段,所有的宏名称都会被对应的文本替换,没有类型信息。例如:#define PI 3.14
    • const关键字: const用于创建常量,具有类型信息,并且在编译时进行类型检查。例如:const double PI = 3.14;。const创建的常量在内存中有分配空间,而宏定义只是简单的文本替换。
  2. 定义和声明的关系:
    • 定义: 定义是创建一个变量、函数、或其他实体,并分配相应的存储空间。例如:int x = 5;
    • 声明: 声明是告诉编译器某个实体的存在而不进行实际的创建。例如:extern int x;
    • 关系: 定义包含声明,但声明不一定包含定义。如果在某个文件中声明了一个变量,而在另一个文件中定义了该变量,那么编译时需要链接这两个文件。

C语言_C程序

语言的发展

  1. Procedure
  2. Function
  3. Object
  4. Meta (template)
  5. Component

Visual Studio使用

Solution(解决方案)指的是解决某一问题整个的方案,需要1个或多个Project(项目)来协同完成。不同的项目可以是不同的语言如C++C#等,可以把这些项目放在一个解决方案中联合编译。

项目目录结构

项目编译成功后,可执行exe文件会生成在项目目录下的x64/Debug中,名字为项目名称

第一个C程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h> /* standard input output. */

// forward declarations.
void bar(void);

int main(void)
{
bar();
return 0;
}

void bar(void)
{
printf("Hello\n");
}

前置声明的主要作用体现在:

  1. 编译器可以在代码中检测函数是否正确调用,如检查函数名、返回类型、参数类型。
  2. 如果代码中函数调用书写正确,则通过编译检测。我们可以写和前置声明中一样的调用书写来欺骗编译器,但是后面链接器会进一步检查是否在下面有函数的正确定义

如果我们删除bar函数定义

1
2
3
4
5
6
7
8
9
10
#include <stdio.h> /* standard input output. */

// forward declarations.
void bar(void);

int main(void)
{
bar();
return 0;
}

则链接器报错:

1
2
3
4
5
1>------ 已启动生成: 项目: Project1, 配置: Debug x64 ------
1>Source.c
1>Source.obj : error LNK2019: 无法解析的外部符号 bar,函数 main 中引用了该符号
1>C:\Users\xcg\source\repos\Solution1\x64\Debug\Project1.exe : fatal error LNK1120: 1 个无法解析的外部命令
1>已完成生成项目“Project1.vcxproj”的操作 - 失败。

什么情况下需要前置声明

  1. 如果把函数实现放在调用处的后面,则需要在调用处前面前置声明;
  2. 如果函数在此文件外,则需要在调用处前面前置声明;
  3. 如果函数在系统库中,也需要在调用处前面前置声明,只不过写到了#include<XXX>中去了,比如printf函数包含在stdio.h中;

尖括号和双引号的区别

  1. 尖括号搜索范围小,编译速度快。
  2. 双引号会优先到当前工程路径下去扫描,没有扫描到则去系统库中搜索。

编译链接步骤

  1. 预处理,如#include
  2. 编译,把每一个.c文件生成一个.obj文件,即目标文件,是CPU可识别的机器码,但无法直接执行。
  3. 汇编
  4. 链接,把所有的.obj文件组合为.exe文件(Linux下为.out

.exe文件由什么组成

  1. 所有.obj文件(自己编写的内容,用户库)
  2. 系统库内容,如printf函数
  3. C启动代码
    1. 首先需要一个调用者来调用main函数,程序才能从入口启动
    2. 其次,有一些全局变量,需要启动代码在main函数执行前加载

C语言程序顶层角度

程序由模块组成,即一个个功能单元。可以说:大的工厂分了好多车间,各个车间有各自的原料。那么,在程序里,语句把这些原料(数据),按多种方法(顺序、分支、循环)送到某一个位置。而表达式则是这些原料(数据)的载体。表达式由运算符、数据组成。

1
2
3
4
5
6
7
8
C Program
functions
statements
expressions __
| \
| \
operators \
elementary data type