链接
链接阶段,由链接器完成。
链接器将各个目标文件以及可能用到的库文件进行链接,生成最终的可执行程序。
在这个阶段,链接器会解析目标文件中的符号引用,并将它们与符号定义进行匹配,以解决符号的地址关联问题。
链接方式
我们在say_hello()函数中调用了printf()函数,这个函数是在stdio.h中声明的,后者来源于glibc库,printf()的实现在glibc的二进制组件中,通常是在共享库(如libc.so)或静态库(libc.a)文件中。
因此,我们除了要链接main.o hello.o,还需要和glibc库的文件链接。
通常,C语言的链接共有三种方式:静态链接、动态链接和混合链接。
静态链接
将所有目标文件和所需的库在链接时一起打包进最终的可执行文件。
库的代码被复制到最终的可执行文件中,使得可执行文件变得自包含,不需要在运行时查找或加载外部库。
gcc -static main.o hello.o -o main
-static参数指示编译器进行静态链接,而不是默认的动态链接。
使用这个参数,GCC会尝试将所有用到的库函数直接链接到最终生成的可执行文件中,包括C标准库(libc)。
动态链接
库在运行时被加载,可执行文件包含了需要加载的库的路径和符号信息。动态链接的可执行文件比静态链接的小,因为他们共享系统级的库代码。与静态链接不同,库代码不包含在可执行文件中。
gcc main.o hello.o -o main
没有添加-static关键字,gcc默认执行动态链接,即glibc库文件没有包含到可执行文件中。
我们也可以将自己编写的部分代码处理为动态库
将hello.o编译为动态链接库libhello.so。
gcc -fPIC -shared -o libhello.so hello.o
- -fPIC:这个选项告诉编译器为位置无关码生成输出。在创建共享库时使用这个选项非常重要,因为它允许共享库被加载到内存中的任何位置,而不影响其执行。这是因为位置无关码使用相对地址而非绝对地址进行数据访问和函数调用,使得库被不同程序加载时能够灵活映射到不同的地址空间。
- -shared:这个选项指示GCC生成一个共享库而不是一个可执行文件。共享库可以被多个程序同时使用,节省了内存和磁盘空间。
- -o libhello.so:这部分指定了输出文件的名称。-o选项后面跟着的是输出文件的名字,这里命名libhello.so。按照惯例,Linux下的共享库名称以lib开头,扩展名为.so(表示共享对象)。
使用GCC,采用位置无关代码的方式,从hello.o目标文件创建一个名为libhello.so的动态共享库文件。
使用动态链接库编译新的可执行文件:
gcc main.o -L ./ -lhello -o main_d
- L./:指定了库文件搜索路径,-L选项告诉编译器在哪些目录下查找库文件。
- -lhello:指定了要链接的库。-l选项后面跟库的名称。链接器会搜索名为libhello.so(动态库)或libhello.a(静态库)的文件来链接。
执行的时候还要指明动态库路径。
LD_LIBRARY_PATH=/home/atguigu/helloword ./main.d
混合链接
自己写的库静态链接,其它库动态链接。
将hello.o编译为静态链接库libhello.a
ar crv libhello.a hello.o
笔试题
用预处理指令#define声明一个常数,用以表明一年中有多少秒
#define SECONDS_PER_YEAR (365*24*60*60)UL
主要是后缀表示无符号长整形,用于确保计算结果不会溢出,可以存储更大的数值。
下一个标准宏MIN,这个宏输出两个参数并返回较小的一个
#define MIN(A,B) ((A) <=(B) ? A:B)
当然,使用宏也是有副作用的,
MIN(*p++,b)的作用结果是((*p++) <= (b) ? (*p++) : (b))这个表达式会产生副作用,指针p会作两次++自增操作。
一个有10个指针的数组,该指针指向一个函数,该函数有一个整型参数并返回一个整型数。
int (*p[10])(int)
关键字static的作用
修饰静态局部变量
生命周期:从程序运行开始一直到程序结束。
作用域:仅限于定义它的函数内部。
初始化:仅初始化一次,下次进入函数时,保留上次的值。
用途:用于保存函数调用之间的数据,例如计数器、标志位等。
静态全局变量
生命周期:从程序开始运行一直到程序结束。
作用域:仅限于定义它的文件内部。
初始化:只初始化一次。
用途:实现模块内的变量共享,避免命名冲突。
修饰类成员
静态数据成员:属于类,不属于类的任何一个对象。所有对象共享同一个静态数据成员。用途:表示类的共有属性,例如类的计数器。
静态成员函数:与静态成员类似,可以通过类名直接调用。只能访问静态成员。用途:实现一些与具体对象无关的操作,比如类的初始化和清理工作。
const关键字的作用
const表示常量,即不可修改。通过使用const,可以提高程序的可靠性,避免无意中的数据修改,并为编译器优化提供更短信息。
修饰变量
一旦初始化,它的值就不能更改。
常量指针
指针指向的值不能修改,但指针的指向可以修改。
指针常量
指针的指向不能修改,但指针指向的值可以修改。
常量引用
引用本身是常量,不能重新绑定到其它对象。
修饰函数参数
防止函数意外修改参数。
修饰函数返回值
表明返回值不能被修改。
修饰类成员函数
表明函数不会修改对象的状态。
前两个的作用是一样,a是一个整型常量。
const还可以表示该函数返回一个常量,放在函数的返回值的位置。
在类成员函数的声明和定义中,const放在函数的参数表之后,不能修改该对象的数据成员。
volatile含义
volatile关键字告诉编译器,一个变量的值可能会在程序的控制和监测之外被改变。
这个变量的值可能随时发生变化,而不依赖于本身的代码。编译器在遇到被volatile关键字修饰的变量时,不会对它进行优化,每次使用该变量时会直接从内存中读取,而不是使用寄存器中的缓存值。
为什么要使用volatile?
- 多线程环境:在多线程编程中,多个线程可能同时访问同一个变量,volatile可以确保每个线程都读取到变量的最新值,避免出现数据不一致的问题。
- 中断服务程序:中断服务程序可能会修改全局变量,而主程序也可能访问这些变量。使用volatile可以保证主程序每次读取全局变量时都得到最新的值。
- 硬件寄存器:很多硬件设备的寄存器都是通过内存映射的方式进行访问的。由于外部影响,这些寄存器的值随时可能发生变化,使用volatile可以确保每次访问寄存器时都读取到最新的值。
一个参数既可以是const还可以是volatile吗?
例如,只读的状态寄存器。它是volatile因为它可能被意想不到地改变,它是const意味着软件不能修改它。
移动零
给定一个数组nums,将一个函数所有0移动到数组的末尾,同时保持非零元素的相对顺序。
双指针
左指针指向已经处理好的序列的尾部,右指针指向待处理序列的头部。
每次右指针遇到非零值,左指针和右指针的值交换,同时左指针向右移动。
左指针左边均为非零数。右指针左边知到左指针处均为零。
如果数组中没有零,则快慢指针一直指向相同位置,如果数组中有零,快指针先走,此时慢指针指向0,所以需要交换。
排序链表
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* sortList(ListNode* head) {
if(head == nullptr){
return nullptr;
}
multiset<int> set1;
while(head){
set1.insert(head->val);
head = head->next;
}
ListNode* newHead = new ListNode(*set1.begin());
ListNode* tempNode = newHead;
auto p = set1.begin();
for(p++; p!=set1.end(); p++){
tempNode->next = new ListNode(*p);
tempNode = tempNode->next;
}
return newHead;
}
};
对链表进行插入排序
给定单个链表的头 head ,使用 插入排序 对链表进行排序,并返回 排序后链表的头 。
从前往后插入点
插入排序的基本思想是,维护一个有序序列,初始时有序序列只有一个元素,每次将一个新的元素插入到有序序列中,将有序序列的长度增加 1,直到全部元素都加入到有序序列中。
如果是数组的插入排序,则数组的前面部分是有序序列,每次找到有序序列的第一个元素(待插入元素)的插入位置,将有序序列中的插入位置后面的元素都往后移动一个位置,然后将插入元素置于插入位置。
- 首先判断给定的链表是否为空,若为空,不需要进行排序,直接返回。
- 创建哑节点dummyHead,令dummyHead.next = head。引入哑节点是为了便于在head节点之前插入节点。
- 维护lastSorted为链表的已排序部分的最后一个节点,初始时lastSOrted=head。
- 维护curr为待插入的元素,初始时curr = head.next。
- 比较lastSorted和curr的节点值。
若lastSorted,val <= curr.val,说明curr应该位于lastSorted之后,将lastSorted后移一位,curr变成新的lastSorted。
否则,从链表的头节点开始往后遍历,寻找插入curr的位置。
排序链表
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* sortList(ListNode* head) {
if(head == nullptr){
return nullptr;
}
ListNode* dummyNode = new ListNode(-1, head);
ListNode* sortedNode = head;
ListNode* curr = head->next;
while(curr){
if(sortedNode->val <= curr->val){
sortedNode = sortedNode->next;
}else{
ListNode* prev = dummyNode;
while(prev->next->val <= curr->val){
prev = prev->next;
}
sortedNode->next = curr->next;
curr->next = prev->next;
prev->next = curr;
}
curr = sortedNode->next;
}
return dummyNode->next;
}
};
插入排序时间复杂度是O(N2),其中N是链表的长度。
要求(O*nlogn)的时间复杂度和O(1)的空间复杂度,时间复杂度是O(nlogn)的排序算法包括归并排序、堆排序和快速排序(快排最差时间复杂度是O(N²))
最适合链表的排序算法是归并排序。
归并排序基于分治算法。最容易想到的实现方式是自顶向下的递归实现,考虑递归调用的栈空间,自顶向下归并排序时间复杂度是O(log n)。如果要达到 O(1) 的空间复杂度,则需要使用自底向上的实现方式。
合并两个有序链表
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {
ListNode* dummyNode = new ListNode(-1);
ListNode* sorted = dummyNode;
ListNode* p1 = list1;
ListNode* p2 = list2;
while(p1 && p2){
if(p1->val <= p2->val){
sorted->next = p1;
p1 = p1->next;
}else{
sorted->next = p2;
p2 = p2->next;
}
sorted = sorted->next;
}
while(p1){
sorted->next = p1;
sorted = sorted->next;
p1 = p1->next;
}
while(p2){
sorted->next = p2;
sorted = sorted->next;
p2 = p2->next;
}
return dummyNode->next;
}
};
自顶向下归并排序
对链表自顶向下归并排序的过程如下。
- 找到链表的中点,以中点为分界,将链表拆分成两个子链表。寻找链表的中心可以使用快慢指针的做法,快指针每次移动2步,慢指针每次移动1步,当快指针到达链表末尾时,慢指针指向的链表节点即为链表的中点。
- 对两个子链接分别排序。
- 将两个排序的子链表合并,得到完整的排序后的链表。
单词搜索
给定一个mxn的二维字符网格board和一个字符串单词word。
如果Word位于网格中,返回true。