编程语言知识点一
[TOC]
编程语言知识点一
一、在main执行之前和之后的操作
在 C++ 程序中,
main函数虽然是程序员编写代码的入口,但并不是程序运行的真正起点,也不是终点。在main执行前后,操作系统和 C++ 运行时库(C Runtime Library, CRT)会进行一系列重要的初始化和清理操作。
1、在 main 执行之前的操作(初始化阶段):
在控制权移交给
main之前,程序主要进行静态资源的初始化和运行环境的准备。
a、所有静态存储期的变量初始化
在 main 之前初始化的:1、全局变量 2、全局 static 变量 3、全局对象 4、全局 static 对象 5、类的 static 成员变量(这个 static 变量属于类,而不是对象)
不在 main 之前初始化的:局部的 static 变量
1 |
|
b、GCC/Clang 特有扩展:Constructor 属性
GCC 和 Clang 编译器允许使用
__attribute__((constructor))将普通函数标记为在main之前运行。这常用于库的初始化。过度依赖这种自动初始化可能导致程序的可维护性下降,特别是在程序比较复杂时。
1 | __attribute__((constructor)) void preMain() { printf("I run befor |
c、 动态链接库(DLL/Shared Object)的加载
动态链接库(Dynamic Link Library,简称 DLL)是 Windows 操作系统中使用的一种文件格式,而在类 Unix 操作系统中,类似的文件通常被称为共享对象文件(Shared Object,简称 .so)。动态链接库的主要作用是允许多个程序共享库中的代码和资源,从而节省内存和存储空间,并允许程序在运行时根据需要加载和链接库。
与静态链接库(Static Library)不同,静态链接库的代码和数据在编译时就被直接复制到应用程序的执行文件中,而动态链接库的代码是在程序运行时通过操作系统加载的。动态链接库通常包含一些通用的功能,像图形处理、网络通信、数据库操作等,多个程序可以共享它们,从而减少了程序的大小和重复性。
如果程序依赖动态库,操作系统的加载器(Loader)会先加载这些库,并执行库中的初始化代码(如全局对象的构造)。
- (由操作系统隐式完成,但如果在代码中显式加载)
1 | // 动态库中的 _init() 函数或全局变量会在库加载时(通常在 main 前)执行 |
d、 初始化运行时环境(CRT Startup)
这是由编译器插入的引导代码(通常叫
_start)。它负责设置栈(Stack)、初始化堆(Heap)、初始化 I/O 缓冲区、以及解析命令行参数(argc,argv)。
(这是汇编层面的操作,C++ 中表现为 main 的参数准备好)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22_start:
↓
设置栈指针(Stack Pointer)
↓
调用 __libc_start_main()
↓
初始化:
- 全局变量
- 静态变量
- C++ 全局对象构造函数
- malloc 内部结构
- I/O 缓冲区(stdout 缓冲模式)
- TLS(线程本地存储)
↓
准备 main 的参数 argc, argv
↓
调用 main(argc, argv)
↓
等 main 返回,接着调用 exit()
- 调用 static/global 对象的析构函数
- 调用 atexit 注册的回调函数
- 清理堆内存等
2、 main 执行之后的操作(清理阶段)
当
main函数执行return,或者调用exit()时,程序并未立即结束,而是进入清理阶段。
a. 注册的 atexit 函数
C++ 标准允许使用
std::atexit注册函数,这些函数会按照“后注册先执行”(LIFO,栈的顺序)的顺序在main结束后被调用。
1 | std::atexit(myCleanupFunction); // 该函数会在 main 返回后被系统自动调用 |
b. 全局变量和静态变量的销毁
与初始化顺序相反,所有全局对象和静态对象的析构函数会在
main结束后被调用。
1 | // 之前定义的 globalObject 的析构函数 ~MyClass() 会在此刻执行 |
c. GCC/Clang 特有扩展:Destructor 属性
类似于构造属性,被标记为 destructor 的函数会在
main结束后执行。
1 | __attribute__((destructor)) void postMain() { printf("I run after main!\n"); } |
4. 刷新 I/O 缓冲区
系统会关闭打开的文件描述符,并强制刷新标准输出(stdout/stderr)缓冲区,确保所有未输出的日志都被写入文件或屏幕。
1 | // C++ 运行时会自动执行 fflush(stdout) 和 fclose(all_streams) |
二、指针 VS 引用
指针和引用的区别是 C++中最重要最经典的知识点,简单的来说:指针是一个存储地址的变量,而引用是原变量的一个别名(类似于别人给你取的绰号)
1、核心区别
a、初始化和空值(这是最大的区别)
指针: 可以不初始化(虽然危险),也可以设为 nullptr(空指针)。它可以在程序运行中暂时不指向任何东西。
引用: 必须在定义时立刻初始化,且绝对不存在“空引用”(Null Reference)。引用一旦诞生,就必须属于某个对象。
1 | int a = 10; |
b、可变性
指针: 是一个自由的变量。它今天可以指向 A,明天可以指向 B。
引用: 是“从一而终”的。一旦引用绑定了变量
a,它就永远是a的别名。如果你给引用赋值,不是改变引用指向谁,而是改变原变量的值。
1 | int x = 5; |
#### c、内存地址和自身实体
指针: 是一个独立的实体。指针本身在内存中占用空间(32位系统占4字节,64位系统占8字节),指针也有自己的地址。
引用: 在 C++ 语法层面,它不是一个独立的对象,它只是原对象的另一个名字。对引用取地址(
&r),得到的就是原对象的地址。
1 | int a = 10; |
d、多级间接
指针: 可以有多级。
int**(指向指针的指针),int***等等,合法且常见。引用: 只有一级。没有“引用的引用”(
int&&是右值引用,那是另一个概念,不是指向引用的引用)。
1 | int a = 10; |
e、sizeof的含义
指针:
sizeof(p)永远返回指针类型的大小(4 或 8 字节)。引用:
sizeof(r)返回的是被引用对象的大小。
1 | double d = 3.14; |
f、自增运算
指针:
p++是指针算术,表示指针向后移动一个单元(指向下一个内存地址)。引用:
r++等同于a++,是对原变量的值进行数值加一。
2、底层实现
虽然 C++ 标准说引用不是对象,但在编译器的汇编实现层面: 引用通常就是作为一个
const指针(指针常量)来实现的。
当你写:
1 | int& r = a; |
编译器在底层生成的汇编代码,和下面这段代码几乎是一模一样的:
1 | int* const r_ptr = &a; // 指针常量:指针本身不能变,只能一直指向 a |
区别在于: 指针需要你手动写 *p 来解引用,而引用由编译器自动解引用。
3、什么时候用哪个?
(能用引用,尽量用引用;必须用指针时,才用指针。)
a、优先使用引用(Reference)
函数参数传递: 为了避免拷贝大对象(如
std::vector,std::string或自定义类),使用const Type&。1
void printImage(const Image& img); // 高效且安全,像传值一样方便
函数返回值: 当需要实现链式调用(如
operator<<)或修改容器内元素时。运算符重载:
operator[]等通常返回引用。
b、 必须使用指针(Pointer)(在用完之后记得释放指针,不然会内存泄露)
- 表示“可选”的存在: 如果这个参数可以为空(nullptr),必须用指针。
- 动态内存管理: 虽然现在推荐用智能指针(
std::unique_ptr),但本质还是指针。 - 数组操作/缓冲区: C 风格的 API 交互。
- 需要改变指向: 比如链表(Linked List),节点需要断开连接并指向新节点,引用做不到(因为引用不能重新绑定)。
| 特性 | 指针 (Pointer) | 引用 (Reference) |
|---|---|---|
| 本质 | 存储地址的变量 | 对象的别名 |
| 符号 | * |
& |
| 初始化 | 可选 (建议初始化) | 必须初始化 |
| 空值 | 可以是 nullptr |
不可以为空 |
| 重置指向 | 可以随时改变指向 | 一旦绑定,终身不改 |
| 语法 | 需要 *p 解引用,-> 访问成员 |
直接像原对象一样使用,. 访问成员 |
| 多级 | 有 **p (多级指针) |
无 (没有引用的引用) |
| sizeof | 地址的大小 (4/8 byte) | 原对象的大小 |
| 主要用途 | 动态内存、数据结构(链表)、可选参数 | 函数传参、返回值、消除拷贝 |