1.文件操作

1.1 文本文件

返回FILE结构体:fgets和fprintf以行方式读写文本文件,但不能读写二进制文件。用fread和fwrite可以读写文本文件和二进制文件。
在这里插入图片描述
在这里插入图片描述
vi /tmp/test1.txt,可见有5行记录,不管执行多少次都是5行记录,因为文件打开方式是w,每次打开文件时都会清空原文件中的记录。
在这里插入图片描述
如下301可换成sizeof(strbuf),300字符(字符串结束符\0)+1个换行符。
在这里插入图片描述

int main() {
    int fd;
    char buffer[5] = {0};
    ssize_t bytesRead;

    fd = open("/home_a/abcd", O_RDONLY, 0444);
    bytesRead = read(fd, buffer, 4);
    if (bytesRead == -1) {
        printf("read error");
        return 1;
    }
    printf("Read %zd bytes: %s\n", bytesRead, buffer);  //Read 4 bytes: 0xff
    return 0;
}

#include <stdio.h>
int main() { // a.c
    char str[100];
    printf("请输入一行字符串: ");
    if (fgets(str, sizeof(str), stdin) != NULL) {
        char *newline = strchr(str, '\n'); // strchr: 查找\n以及后面字符
        printf("换行符:%s\n", newline);
        if (newline != NULL) {
            *newline = '\0'; //替换为结束符
        }
        printf("你输入的字符串是: %s\n", str);
        return 0;
    }
}

在这里插入图片描述

1.2 二进制文件

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

1.3 文件定位

文件内部有一个位置指针,用来指向当前读写的位置,也就是读到第几个字节。在文件打开时,如果打开模式是r和w,位置指针指向文件的第一个字节。如果打开模式是a,位置指针指向文件的尾部,每当从文件里读n个字节或文件里写入n个字节后,位置指针会后移n个字节。
在这里插入图片描述

FILE *fptime;
fptime=fopen("/tmp/time","w");
time_t time_log = time(NULL);
struct tm* tm_log = localtime(&time_log);
fprintf(fptime, "flag[%d]  LINE[%d]    %04d-%02d-%02d %02d:%02d:%02d\r\n",sensor_flag, __LINE__, tm_log->tm_year + 1900, tm_log->tm_mon + 1, tm_log->tm_mday, tm_log->tm_hour, tm_log->tm_min, tm_log->tm_sec);
fflush(fptime);
fclose(fptime);

FILE *fpLedColor=fopen(led_color,"w");
fseek(fpLedColor,0,SEEK_SET);  // SEEK_SET:从文件开头开始计算偏移量
fprintf(fpLedColor,"%s",sensor_flag?LED_GREEN_CODE:LED_YELLOW_CODE);
fprintf(stderr,"11\n"); // 打印在控制台,cpp也可用
fflush(fpLedColor);
fclose(fpLedColor);

2.动静态库

编译【预处理(语法检查),编译(.c->.s汇编文件),汇编(.s->.o二进制文件),链接(多个.o合并成1个执行文件)】的最后阶段即将依赖引入过程叫链接,静态链接同【notes7】2.2章节,so打到二进制文件里,跨机器不用机器上有so文件。

动态链接的so文件通过mmap加载进共享内存,动态链接a.out文件小且内存占用小,此外动态链接在so库更新后不需重新编译,一般首选。很多进程用到C语言libc.so里stdio.h里打印函数,如果通过静态链接,每个二进制进程占一个libc.so,占用的内存多。
在这里插入图片描述
和内核交互除了syscall,大部分用库函数如libc,libc是一个标准有很多实现:g(gnu)libc【ubuntu】,musllibc【alpine】
在这里插入图片描述
如上默认编译链接是动态,如下static指定静态链接。gcc是gnu的编译工具集合,gcc不光编译c语言且支持很多平台
在这里插入图片描述
如下alpine系统没有glibc库。
在这里插入图片描述

2.1 静态库

公用函数库的public.cpp是源代码,对任何人可见,实际开发出于保密并不希望提供公用函数库源代码。C/C++提供了一个保证代码安全性方法,public.cpp编译成库。

// public.h
#ifndef PUBLIC_H
#define PUBLIC_H 1
#include <stdio.h>
void func();   // 自定义函数的声明
#endif
// public.cpp
#include "public.h"
void func()   // 自定义函数的实现
{
  printf("我心匪石,不可转也。我心匪席,不可卷也。威仪棣棣,不可选也。\n");
}
// book265.cpp
#include "public.h"  // 把public.h头文件包含进来
int main()
{
  func();
}
g++ -o book265 book265.cpp public.cpp
./book265
我心匪石,不可转也。我心匪席,不可卷也。威仪棣棣,不可选也。

链接库的文件名是libpublic.a,链接库名是public,缺点使用的静态库发生更新改变,程序必须重新编译

gcc -c -o libpublic.a public.cpp

使用方法一:直接把调用者源代码和静态库文件名一起编译:

g++ -o book265 book265.cpp libpublic.a

使用方法二:用L参数指定静态库文件的目录,-l参数指定静态库名:如果要指定多个静态库文件的目录,用法是“-L/目录1 -L目录2 -L目录3”;如果要指定多个静态库,用法是“-l库名1 -l库名2 -l库名3”

g++ -o book265 book265.cpp -L/home/w/demo -lpublic
./book265
我心匪石,不可转也。我心匪席,不可卷也。威仪棣棣,不可选也。

2.2 动态库

动态库发生改变,程序不需要重新编译,动态库升级方便。

g++ -fPIC -shared -o libpublic.so public.cpp

如果在动态库文件和静态库文件同时存在,优先使用动态库编译:

g++ -o book265 book265.cpp -L/home/w/demo -lpublic

执行程序./book265时,出现以下提示:/book265: error while loading shared libraries: libpublic.so: cannot open shared object file: No such file or directory,因为采用了动态链接库的可执行程序在运行时需要指定动态库文件的目录,Linux系统中采用LD_LIBRARY_PATH环境变量指定动态库文件的目录。采用以下命令设置LD_LIBRARY_PATH环境变量。

export LD_LIBRARY_PATH=/home/w/demo:.

如果要指定多个动态库文件的目录,用法是“export LD_LIBRARY_PATH=目录1:目录2:目录3:.”,目录之间用半角的冒号分隔,最后的圆点指当前目录。接下来修改动态库中func函数的代码:

// printf("我心匪石,不可转也。我心匪席,不可卷也。威仪棣棣,不可选也。\n");
printf("生活美好如鲜花,不懂享受是傻瓜;\n");

如下重新编译动态库,无需重新编译book265,直接执行程序。

g++ -fPIC -shared -o libpublic.so public.cpp
./book265
生活美好如鲜花,不懂享受是傻瓜;

2.3 编译时为什么要加上 –lm

// 代码一
#include <stdio.h>
#include <math.h> //exp, man exp:Link with -lm
int main(int argc, char const *argv[]){
   printf("The exponential value of %lf is %lf\n", 0, exp(0));
   printf("The exponential value of %lf is %lf\n", 0+1, exp(0+1)); //e的1次幂
   printf("The exponential value of %lf is %lf\n", 0+2, exp(0+2));
   return(0);
}

在这里插入图片描述

// 代码二
#include <stdio.h>
#include <math.h>
int main(int argc, char const *argv[]){
   double x = 0;
   printf("The exponential value of %lf is %lf\n", x, exp(x));
   printf("The exponential value of %lf is %lf\n", x+1, exp(x+1));
   printf("The exponential value of %lf is %lf\n", x+2, exp(x+2));
   return(0);
}

在这里插入图片描述
在这里插入图片描述
代码一调用exp传入的参数是常量为0 。代码二调用exp传入的参数是变量 x,代码一会不会在运行之前就计算好了呢?如下代码一没有看到调用exp的身影,当传入参数为常量时就已计算好了值,最后不需调用exp函数。代码二通过如下main.s汇编代码可见多次调用call函数。
在这里插入图片描述
math.h中声明的库函数还有一点特殊之处,gcc命令行必须加-lm选项,因为数学函数位于libm.so库文件中(这些库文件通常位于/lib目录下),-lm选项告诉编译器,程序中用到的数学函数要到这个库文件里找。

gcc a.c -o a.outarm-linux-gcc a.c -o b.out,如果执行out文件出现No such file or directory,则将如下两个so文件互相ln -s建软链接。
在这里插入图片描述

3.信号

getpid库函数功能是获取进程编号,该函数没有参数,返回值是进程的编号(相同程序在不同时间执行,进程编号不同)。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
僵尸进程:一个进程执行了exit系统调用退出时会向父进程发送SIGCHLD信号,而其父进程并没有为它收尸(调用wait或waitpid来获得它的结束状态,如进程ID、终止状态等等)的进程,ps显示有< default >。父处理子进程死后的信息,如果不想处理就需要交给系统处理。
在这里插入图片描述
进程间通信IPC方式:1.管道:ls | grep。FIFO(first in first out)是管道一种。
2.消息队列:内核创建的一个消息队列,os中多个进程都能操作这个消息队列,可以往里面发送消息,可以接收消息,类似socket。
3.共享内存:每个进程访问内存时,有一个虚拟内存地址和物理内存地址的一个映射:一般两个进程的虚拟内存地址可以一样,但映射的物理内存地址一般不一样。共享内存就是将它们映射的物理内存地址也变一样,这时两个进程能同时访问一块相同的物理内存,借助这块物理内存实现通信。
4.套接字socket:访问数据库进程和数据库进程本身,这两个进程间通信就是通过3306号端口建立起的tcp套接字。本机访问mysql不走tcp的套接字,而是走unix域套接字(UDS,不用ip)。
5.信号量/灯:类似一个计数器,控制多个进程对一个共享资源的访问,起到控制数量的锁机制。
6.信号:一个进程可向另一个进程发送一个信号,进程可处理这个信号。如下列出所有信号,linux中信号大多数是把另一个进程杀死,干脆把这个指令叫kill了,如下64种死法。 如键盘中断ctrl+c是当前shell向tail -f这个进程发送一个信号值为2的SIGINT的信号,如用kill+进程id命令是15,kill -9是9。
在这里插入图片描述
捕捉信号2:在死循环之前注册下信号的处理,如下让ctrl+c无效。
在这里插入图片描述
捕捉了ctrl+c,无法停止,只能用kill。
在这里插入图片描述
在这里插入图片描述
程序后台运行两种方法:&:ctrl+c无法中止,用killall book1或kill 进程号。if (fork()>0)return 0 父进程退出
在这里插入图片描述
信号作用:服务程序在后台运行,如果想终止它,杀了它不是个好办法,因为没有释放资源。如果能向程序发一个信号,程序收到这个信号后调用一个函数,在函数中编写释放资源代码。
在这里插入图片描述
下面 EXIT函数就是自定义函数,TcpServer设为全局变量因为EXIT函数要访问它并关闭socket。
在这里插入图片描述

4.socket

http(client和server间req和res),socket(client和server间长连接)。
在这里插入图片描述
accept第一个参数是listen fd,第二个参数是addr(如果不关心对端地址,直接填 NULL。非空时把对端 IP/端口填进去,格式跟bind一致),第三个参数是addrlen。listen fd是门卫,accept 把排到队的客人拉进来,发给你一张新门票(clientfd),以后只跟这张门票打交道。
在这里插入图片描述
在这里插入图片描述
一个字节是2的8次方即256,所以0-255。ipv4组合方式共2的32(4*8=32)次方即43亿个。B/S(浏览器对服务器)和C/S(客户端对服务器)。
在这里插入图片描述
下面将三要素融合,https中s就是secure。
在这里插入图片描述
如下文件上传:上面是服务端,socket封装了io流,所以要关。高层socket关了,低层也会关。
在这里插入图片描述

import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;

public class UploadClient {
    public static void main(String[] args) throws IOException {
        Socket socket = new Socket("127.0.0.1", 10086);

//11111111111111111111111111111111111111111111111111111111111111111111111111111
        FileInputStream fis = new FileInputStream("柳岩.jpg"); //读本地文件        
        OutputStream os = socket.getOutputStream();    

        int length = -1;
        byte[] buffer = new byte[1024];
        while((length = fis.read(buffer)) != -1){ //读
            os.write(buffer,0,length); //写到服务器socket中
        }
        fis.close();
        socket.close();
    }
}

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.UUID;
public class UploadServer {
    public static void main(String[] args) throws IOException {        
        ServerSocket serverSocket = new ServerSocket(10086); //只创建一个服务端,一个端口不能被两个程序占用,不能在这行上面加while(true)
        while(true){  //死循环接收客户端请求            
            Socket socket = serverSocket.accept(); //虽然while(true),但这行会阻塞,单线程
            new Thread(new Runnable() { //不能放accept()前面,不能无限开线程
                @Override
                public void run() {
                    try {
                        InputStream is = socket.getInputStream();                        
                        // long time = System.currentTimeMillis(); // 以系统当前时间命名,同一毫秒,有两个用户同时上传文件, 就会有用户文件覆盖
                        String name = UUID.randomUUID().toString().replace("-", ""); //下行time改为name
                        FileOutputStream fos = new FileOutputStream("c:/test/server/" + name + ".jpg");
                        
                        int length = -1;
                        byte[] buffer = new byte[1024];
                        while((length = is.read(buffer)) != -1){
                            fos.write(buffer,0,length);
                        }                        
                        fos.close(); //应该放在finally中
                        socket.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }).start();
//         serverSocket.close(); // 服务器不能关闭,关了就不回到while(true)了
        }
    }
}

如下BS案例:socket被写,页面被读。写一个浏览器程序难度和操作系统一样,鸿蒙os就是浏览器。
在这里插入图片描述

import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;

public class BsServer {
    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = new ServerSocket(10090);
        Socket socket = serverSocket.accept(); //接收浏览器请求
        
//1111111111111111111111111111111111111111111111111111111111111111111111111111
        OutputStream os = socket.getOutputStream(); //写出页面
        os.write("HTTP/1.1 200 OK\r\n".getBytes()); // http协议的响应行  第一行
        os.write("Content-Type:text/html\r\n\r\n".getBytes()); //http协议的响应头 第二行,第三行为空白
        FileInputStream fis = new FileInputStream("index.html"); //已有,用来被读

        int length = -1;
        byte[] buffer = new byte[1024];
        while((length = fis.read(buffer)) != -1){
            os.write(buffer,0,length);  //第四行,响应体
        }
        fis.close();
        socket.close();
        serverSocket.close();
    }
}

在这里插入图片描述
在这里插入图片描述

5.std&boost库

下载boost库解压后执行编译和install脚本,一般.so或.a自动装在/usr/local/lib下。
在这里插入图片描述

int main(){        
	const size_t n = 100;       
	int v[n];
	// 标准C: 引入一个变量 i, 辅助完成循环访问 v 中每个元素的任务,循环条件上也必须写明循环终止条件,否则容易发生栈溢出错误
	size_t i;        
	for (i = 0; i < n; i++) { v[i] = 0;}           
	// c++17: 隐藏了迭代的细节(编译器保证正确)
	for (auto& x : v) {x = 0;}
}
std::pair<int, int> divide(int dividend, int divisor) {
    int quotient = dividend / divisor; // 除数
    int remainder = dividend % divisor;  // 余数
    return std::make_pair(quotient, remainder);
}
int main() {
    std::pair<int, int> result = divide(10, 3);
    std::cout << "Quotient: " << result.first << ", Remainder: " << result.second << std::endl;  // Quotient: 3, Remainder: 1
    return 0;
}
int main() {
    std::string str1 = "Hello";
    std::string str2 = std::move(str1);
    std::cout << "str1: " << str1 << std::endl;  // 此时 str1 的内容已经被移动,为空
    std::cout << "str2: " << str2 << std::endl;  // 输出 "Hello"
    return 0;
}

#define INFO_LOG Logger<true>(std::cout)  // true为false时,日志代码在编译期被完全移除
// Logger 类的构造函数需要一个 std::ostream&(输出流引用)参数,std::cout是标准输出流的全局实例,代表控制台输出
template <bool active>
struct Logger
{
    std::ostream& stream;
    Logger(std::ostream& _stream) : stream(_stream)
    {
        if constexpr (active)  // true
        {
            stream << std::dec; // 强制设为十进制格式,C++流对象(如 std::cout)保留格式状态(如十六进制、八进制等)
        }
    }
    template <typename T>   // 模板参数支持任意数据类型
    Logger& operator<<(const T& value)
    {
        if constexpr (active)
        {
            stream << value;   // 实际输出
        }
        return *this;
    }
    ~Logger()
    {
        if constexpr (active)
        {
            stream << std::endl; // 输出换行并刷新缓冲区
        }
    }
};
int main() {
    INFO_LOG << "Starting SPD ownership";  // 串口打印
    INFO_LOG << std::hex << 255;  // 输出 "255" 而不是 "ff"(构造函数中的std::dec会重置格式)
}

int main() {
    std::cout << "222" << std::endl;
}
void a()
{
    std::cout << "111" << std::endl;
}
void a() __attribute__((constructor));  // main函数之前执行

$ g++ a.cpp -o a
$ ./a
111
222
int main(int argc, char **argv) {
    static std::deque<bool>liquidStatus = {true, true, true};
    int status = 0; // 如下0 false,1 true
    bool nowLiquidStatusFlag = status ? true : false;
    std::cout << "nowLiquidStatusFlag is : " << std::boolalpha << nowLiquidStatusFlag << std::endl;
    liquidStatus.push_back(nowLiquidStatusFlag);
    liquidStatus.pop_front();  // 移除队列最前面的元素

    std::cout << "Current liquidStatus: ";
    for (bool val : liquidStatus) {
        std::cout << val << " "; // 最后空格
    }
    std::cout << std::endl; // 换行

    uint8_t data = 0x1c;
    if (data & 0x10) // bit4=1,不用>>4,非0进入
    {
        std::cout << "val非0" << std::endl;
    }
    return 0;
}

在这里插入图片描述

std::string text = "ID: 123";               // 待匹配的字符串
std::regex pattern(R"(ID:\s(\d+))");        // 定义正则表达式
// \s 匹配 任意空白字符(如空格 、制表符 \t、换行符 \n 等)
// () 表示 捕获组,匹配的内容会被单独存储,可以通过 match[1] 访问
std::smatch match;                          // 存储匹配结果
if (std::regex_search(text, match, pattern)) {  // 执行匹配
    std::cout << "整个匹配: " << match[0] << std::endl;  // "ID: 123"
    std::cout << "捕获组 1: " << match[1] << std::endl;  // "123"  // 等于std::string indexStr = *(match.begin() + 1);
} else {
    std::cout << "未匹配" << std::endl;
}

// std::string_view对字符串的处理,不用把字符串拷贝,直接赋值就行
// 调用dbus获取property时是字符串类型还是uint8/16等都可以用std::any或std::variant,不会崩溃

5.1 boost.asio实现异步inotify事件监听

// a.cpp
#include <fcntl.h>
#include <sys/inotify.h>
#include <boost/asio.hpp>
#include <functional>
#include <iostream>
#include <memory>

static constexpr const size_t eventBufSize = 1 * (sizeof(struct inotify_event) + NAME_MAX + 1);  // 选择一个最小的缓冲区大小,恰好装下一个满事件。

static void onEventReceived(struct inotify_event* event)  // 处理单个事件
{
    std::string_view fileName(event->name);
    std::cout << fileName << std::endl;
}

static void rawEventHandler(  // async_read_some() 的原始 handler ,需要处理一次读到多个事件的情况,并安排“异步递归”
    std::shared_ptr<boost::asio::posix::stream_descriptor> stream, char* buf,
    const boost::system::error_code& ec, size_t sizeRead)
{
    if (ec)
    {
        std::cerr << "Failed when reading inotify event " << ec.message()
                  << std::endl;
        return;
    }
    char* pos = buf; // 缓冲区中可能存在多个事件,逐一对其进行处理
    while (pos - buf < sizeRead)
    {
        struct inotify_event* event = reinterpret_cast<struct inotify_event*>(pos);  // 取出单个事件event
        onEventReceived(event);
        pos += sizeof(struct inotify_event) + event->len; // inotify_event 是个可变长度结构体,必须加上 len 才能得到其实际长度
    }

    // 本次读取完成,准备下次读取
    stream->async_read_some(boost::asio::buffer(buf, eventBufSize), 
                            std::bind_front(rawEventHandler, stream, buf));
}


static void setupInotify(boost::asio::io_context& io)
{
    static char buf[eventBufSize];  // inotify 事件是变长结构体 inotify_event,需要一块连续且长期有效的缓冲区。写成 static → 放在静态存储区,直到进程退出才释放;
    int fd = inotify_init1(IN_NONBLOCK); // 创建一个inotify fd , IN_NONBLOCK保证后续read()不会阻塞线程
    if (fd < 0)
    {
        std::cerr << "Unable to set up inotify" << std::endl;
        return;
    }

    inotify_add_watch(fd, "/home/bak/test", IN_CREATE); // 把目录 /home/bak/test 挂到 fd 上,只监听 新建文件 事件

    // 如下把裸 fd 包装成 asio 的 AsyncReadStream 对象。
    // 必须用 shared_ptr:
    // async_read_some 是异步操作,回调可能在 setupInotify 返回很久之后才执行;
    // 若 stream 是局部对象,离开作用域就会被析构,fd 被 close(),后续回调里访问已关闭的fd。
    // shared_ptr 把生存期塞进回调闭包里,保证“只要回调活着,stream 就活着”。
    auto stream = std::make_shared<boost::asio::posix::stream_descriptor>(io, fd); 
 
    stream->async_read_some(boost::asio::buffer(buf, eventBufSize),  // async_read_some 需要固定大小的缓冲区,因此选择拿 buffer() 把上面的 buf 包一下
                            std::bind_front(rawEventHandler, stream, buf));  // stream和buf作为rawEventHandler参数,读取 事件即struct inotify_event,将事件存入 buf 中,调用rawEventHandler依次处理buf里的事件
}

int main()
{
// boost.asio异步单线程:替换轮询while(1) 和for循环
// io_context 是一个epoll包装,参考【notes8】,fd有数据调回调
    boost::asio::io_context io;
    setupInotify(io);
    io.run(); // 理论上永不退出
}

// g++ -std=c++20 a.cpp
// ./a.out 一直卡在这里不退出
// /home/bak/test里新增文件,上一行就会打印出文件名

6.raii

作用域(scope):{} 围起来的那一块代码区域。生命周期(lifetime):对象在内存里真正存活的时间段。RAII全称 “Resource Acquisition is Initialization”即资源获取即初始化:将资源的生命周期与对象的生命周期绑定,当对象创建时获取资源,对象销毁时自动释放资源。

class MyClass {
public:
    MyClass() { std::cout << "constructor" << std::endl; }
    ~MyClass() { std::cout << "destructor" << std::endl; }
};
void func()
{
    MyClass c;
    std::cout << "111" << std::endl;
} // 调用析构
int main()
{
    std::cout << "before func()" << std::endl;
    func();
    std::cout << "after func()" << std::endl;
}
// 运行结果: 
before func()                                                                                                              
constructor // 调用func() 构造MyClass
111
destructor //  MyClass的作用域结束,析构
after func()


struct CustomFd
{
  public:
    CustomFd() = delete; // 把“默认无参构造函数”删了
	  ...
    explicit CustomFd(int fd) : fd(fd) {}  // 构造函数
    ~CustomFd()
    {
        if (fd >= 0)
        {
            close(fd);
        }
    }
};
CustomMap Signature::mapFile(const fs::path& path, size_t size)
{
    CustomFd fd(open(path.c_str(), O_RDONLY));
    return CustomMap(mmap(nullptr, size, PROT_READ, MAP_PRIVATE, fd(), 0), size);
}  // fd作为CustomFd实例,在此处超出作用域后,会自动调用析构函数,关闭文件描述符,不会导致文件描述符泄漏。struct RemovablePath析构函数中直接删除tar包,避免空间浪费。

void context::spawn_complete()  {
{
			std::lock_guard l{lock};          
       ...    
} // lock_guard和需要保护的变量放置到同一个作用域(用花括号括起来),lock_guard构造时上锁; 超出作用域后释放锁
}

7.智能指针

在这里插入图片描述

void foo(char *ptr){  // 我们往往会陷入此场景下需不需要调用 free 释放内存,若释放了会不会dobule free的焦虑。克服这种焦虑的方法只能是:API文档说明或者代码中添加注释
	do(ptr);  // (1)  
	free(ptr) // (2)
}
void bar(){        
	char *p = malloc(100);        
	timer(5, foo, p); // 立即返回,5s后执行foo(p)
	free(p); // (3) // (1)可能遇到悬空指针,正确的做法应从(2) 中执行free,资源所有权的转移,使其看上去违背了“谁申请,谁释放”的原则
} 

void foo(int *v){    
	if( v ! = NULL)   // 对指针进行了判空处理     
        *v = 10;   // 存在 use after free 的可能性,不为空不代表没被释放
	free(v)    // 释放指针指向的内存:存在double free的可能性。若不调用free,存在内存泄漏的可能性
}

// 如下cpp智能指针解决下面问题
void foo(std::unique_ptr<int> v)  // 函数参数是一个独占式智能指针,foo管理其生命周期
{      
		if(v) { // 指针判空                    
			*v = 10; // 赋值,因是独占所有权,指针一定有效                        
		} 
} // 指针变量的生命周期结束,自动调用析构函数释放该指针。自动释放,不存在内存泄漏问题。指针别处不可见,不会use after free

如下是怎么正确使用 unique_ptr:

// 值传递 = 所有权交割,必须 std::move :
void foo(std::unique_ptr<MyClass> p){(void)p;}
int main(){
	auto p = std::make_unique<MyClass>(); // 同std::uniqpue_ptr<MyClass> p{new MyClass()}
	foo(p); // 编译报错,没有显式的资源转移表达
	foo(std::move(p));  // 显式表达资源所有权转移,资源转移后p失效,编译通过
}

// 引用传递 = 只给钥匙不改户主,reset 可以偷偷把房子拆了再盖新的,但房产证上还是原来那个人:
void foo(std::unique_ptr<MyClass>& p){             
	p.reset(new MyClass); // 重装指针,使unique_ptr释放原来管理的指针,进而管理一个新指针
}
int main(){        
	MyClass* ptr = new MyClass;          
	std::cout << ptr << std::endl; // 0x5561a66b62b0        
	std::unique_ptr<MyClass> p { ptr };   // 让unique_ptr管理原始指针     
	std::cout << p.get() << std::endl; // 0x5561a66b62b0  打印出 unique_ptr 管理的原始指针,就是ptr
	foo(p);  
	std::cout << p.get() << std::endl; // 0x5561a66b66e0   打印出 unique_ptr 管理的原始指针,因为发生了重装则不再是ptr      
}
std::unique_ptr<int> pr1(new int(10));
std::unique_ptr<double> pr2 = std::make_unique<double>(2.0);

struct MyClass {   
	 MyClass(int val) : value(val){}
	 int value;
};
std::unique_ptr<MyClass> ptr3 = std::make_unique<MyClass>(30);

std::cout << *ptr1 << std::endl; // 打印 10
std::cout << ptr3->value << std::endl; // 打印 30
(*ptr3).value = 50;
std::cout << ptr3->value << std::endl; // 打印 50

7.1 从0实现unique_ptr

struct Student{int age; std::string name;};
void foo(){   
	Student *s = new Student(18, "Jim");
	doSomeThings(s);    
	delete s;
}

class StudentWrapper{
	public:        
		explicit StudentWrapper(Student *ptr = nullptr): ptr_(ptr){}        
		~StudentWrapper(){delete ptr_;} 
		Student* get() const {return ptr_;}    
	private:        
		Student *ptr_;
};
void foo(){    
	StudentWrapper s(new Student(18, "Jim"));
	doSomeThing(s.get());
} // RAII
template <typename T>
class UniquePtr {
public:
    explicit UniquePtr(T* ptr = nullptr) : ptr_(ptr) {}  // : ptr_(ptr) 即 把形参ptr值直接赋给数据成员ptr_
    ~UniquePtr(){delete ptr_;}
    T* get() const {return ptr_;}
    T& operator*() const {return *ptr_;}
    T* operator->() const {return ptr_;}
private:    
		T* ptr_;
};
UniquePtr<Student> ptr1 { new Student(18, "Jim") };
UniquePtr<int> ptr2 { new int(10) };
std::cout << ptr1->name << std::endl;
std::cout << *ptr2 << std::endl;

// 再添加 禁掉拷贝构造和赋值行为,表明独占所有权
UniquePtr(const UniquePtr&) = delete;
UniquePtr& operator=(const UniquePtr&) = delete;

// 实现一个make_unique函数
template <typename T, typename... Args>
UniquePtr<T> make_unique(Args&&... args){return UniquePtr<T>(new T(std::forward<Args>(args)...));}

8.引用

int i ; // 值类型,有地址,可以&i取地址
int* o; // 指针类型
int& ii=i; // 引用类型,必须赋一个值,指向引用的谁

8.1 左右值

cpp将变量分为左值(有地址)和右值(无地址)。
在这里插入图片描述

8.2 为什么要右值引用

在这里插入图片描述
如下接着上面,隐含buffer b=buf,调用打印“拷贝构造”函数即值传递,拷贝构造函数里最后2行开和拷贝内存里数值到当前buf,有开销。为避免开销,只改void dothing(buffer& b),变成引用/左值传递传地址,运行没有任何打印。
在这里插入图片描述
如下传入是临时构造的即右值,void dothing(const buffer& b)也可以(表示这个引用不会被修改即const ,因为右值不能被修改)。
在这里插入图片描述
在这里插入图片描述
当传入右值是:dothing(拷贝构造),右值是将亡值(经历析构),不需要构造里开和拷贝内存,所以添加右值引用构造器即move拷贝构造器。

8.3 copy & swap

如下移动构造里进行swap相当于拷贝将亡值到this。
在这里插入图片描述

8.4 std::move

在这里插入图片描述
remove_reference字面意思去引用,简化如下:
在这里插入图片描述

9.完美转发

传入左值或右值在函数体里也是左值或右值,不变。
在这里插入图片描述
如下在main中,1是右值,却调用了左值构造器。
在这里插入图片描述
在这里插入图片描述

9.1 引用折叠

如下只有最后一行转换后是右值。
在这里插入图片描述

9.2 万能引用

如下不管传入右/左值,都打印万能引用。
在这里插入图片描述

9.3 std::forward

如下满足了完美转发:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
如下是std::forward源码:
在这里插入图片描述

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐