53模板

template,能让你定义一个待编译的模版 让编译器用一组规则为你编译代码

当我写一个函数时,我在函数中使用一个模板,我实际做的事类似蓝图的东西,当我决定调用那个函数时,指定一些参数,他们决定了实际放入模板的代码

简单例子#

#include <iostream> 

//template表明这是个编译时求值的模板,这不是实际的函数,只有在调用时才会创建并编译成源代码
//可以从实际参数中隐式获取T
template<typename T>
void Print(T value)
{
	std::cout << value << std::endl;
} 
int main()
{
	Print(5);
	//"5.5"被隐式转为const char[4]
	Print("5.5");
	Print(3.7);

	std::cin.get();
}

额外补充#

模板定义调用 Print(“5.5”) 时 T 的类型说明
void Print(T value)const char*发生退化。传递的是地址。
void Print(T& value)const char[4]不发生退化。传递的是数组的引用。
void Print(const T& value)char[4]不发生退化。T 本身是 char[4]。
// 它代表:我只接受一个含有 4个char的数组的“引用”
void Print(const char (&value)[4]) {
    // sizeof(value) == 4
    // 编译器知道这个数组刚好就是 4 个字节
}
//数组的首地址
void Print(const char* value) {
    // sizeof(value) == 8 (在64位系统上)
    // 编译器不知道这里有 4 个字符
}

例子2#

#include <iostream> 

//template表明这是个编译时求值的模板,这不是实际的函数,只有在调用时才会创建并编译成源代码(才会实际创建函数)
//可以从实际参数中隐式获取T
template<typename T>
//和上面一个意思
//template<class T>
void Print(T value)
{
	std::cout << value << std::endl;
}

int main()
{
	//显示指定T
	Print<int>(5);
	//让编译器自己推断T的类型
	Print(5);

	std::cin.get();
}

例子3#

52处理多返回值

这篇讨论的是如何在函数返回两个值

cherno推荐的方法是创建一个结构体并返回它

代码预备#

为了演示,本篇使用了string,为了顺便探讨性能(视频中没提到),我写了一个string.h 和string.cpp打印相关信息

//String.h
#pragma once // 防止头文件被重复包含
#include <iostream>

class String
{
private:
    char* m_Buffer;
    unsigned int m_Size;

public:
    // 默认构造函数
    String() : m_Buffer(nullptr), m_Size(0) {}

    // 构造函数
    String(const char* string);

    // 拷贝构造函数(深拷贝)
    //拷贝构造函数 (String a = b;):当你在创建一个新的对象,并用已有的对象初始化它时调用。
    String(const String& other);

    // 析构函数
    ~String();

    // 运算符重载
    char& operator[](unsigned int index);

    // 赋值运算符重载 (a = b;):当两个对象都已经存在(已经构造完毕),你只是想把其中一个的值覆盖给另一个时调用。
    String& operator=(const String& other); 

    // 友元函数:重载 << 运算符
    friend std::ostream& operator<<(std::ostream& stream, const String& string);

    // 测试函数
    unsigned int& mytest() { return m_Size; } // 简单的内联函数可以直接写在类内
};
#include "String.h"
#include <cstring> // 必须包含 cstring 才能使用 strlen 和 memcpy

// 构造函数
String::String(const char* string)
{
	m_Size = strlen(string);
	m_Buffer = new char[m_Size + 1];
	memcpy(m_Buffer, string, m_Size);
	m_Buffer[m_Size] = '\0';
}

// 拷贝构造函数
String::String(const String& other)
	: m_Size(other.m_Size)
{
	std::cout << "Copied String!" << std::endl;
	m_Buffer = new char[m_Size + 1];
	memcpy(m_Buffer, other.m_Buffer, m_Size + 1);
}

//赋值运算符 
String& String::operator=(const String& other)
{
	std::cout << "Assignment Operator Called!" << other << std::endl;

	// 1. 自赋值检查 (防止 s1 = s1 的骚操作)
	if (this == &other)
		return *this;

	// 2. 释放当前对象旧的内存,防止内存泄漏
	delete[] m_Buffer;

	// 3. 申请新空间并拷贝内容
	m_Size = other.m_Size;
	m_Buffer = new char[m_Size + 1];
	memcpy(m_Buffer, other.m_Buffer, m_Size + 1);

	// 4. 返回对象自身,支持链式赋值 (a = b = c)
	return *this;
}

// 析构函数
String::~String()
{
	delete[] m_Buffer;
}

// 下标运算符重载
char& String::operator[](unsigned int index)
{
	return m_Buffer[index];
}

// 友元函数定义(注意:定义时不需要加 String::,也不需要加 friend 关键字)
std::ostream& operator<<(std::ostream& stream, const String& string)
{
	stream << string.m_Buffer;
	return stream;
}

方法1:使用引用#

#include <iostream>
#include "String.h"

void ParseShader(String& vertexSource, String& fragmentSource)
{
	//假设vs,fs最终计算值是abc、def
	String vs = "abc";
	String fs = "def";

	//这两行代码执行的是 字符串赋值(内部会拷贝)
	vertexSource = vs;
	fragmentSource = fs;
}

int main()
{
	String vs, fs;
	ParseShader(vs, fs);
	std::cout << vs << std ::endl;
	std::cout << fs << std::endl;
	std::cin.get();
}
/*
Assignment Operator Called!abc
Assignment Operator Called!def
abc
def

*/

方法2:使用指针#

#include <iostream>
#include "String.h"

void ParseShader(String* vertexSource, String* fragmentSource)
{
	//假设vs,fs最终计算值是abc、def
	String vs = "abc";
	String fs = "def";

	//这两行代码执行的是 字符串赋值(内部会拷贝)
	if (vertexSource) {
		*vertexSource = vs;
	}
	if (fragmentSource) {
		*fragmentSource = fs;
	} 
}

int main()
{
	String vs, fs;
	ParseShader(&vs, &fs);
	//这种方式允许空指针
	//ParseShader(nullptr, &fs);
	std::cout << vs << std::endl;
	std::cout << fs << std::endl;
	std::cin.get();
}
/*
Assignment Operator Called!abc
Assignment Operator Called!def
abc
def
*/

(同类型)方法3:返回(数组)指针#

#include <iostream>
#include "String.h"

String* ParseShader()
{
	//假设vs,fs最终计算值是abc、def
	//vs,fs先在栈上创建(但String成员m_Buffer是指向堆)
	String vs = "abc";
	String fs = "def";

	//这种语法在c++20才能编译通过
	return new String[]{ vs,fs };
} 

int main()
{ 
	//这里不知道数组有多大
	String* result =ParseShader();

	std::cout << result[0] << std::endl;
	std::cout << result[1] << std::endl;
	std::cin.get();
}
/*
Assignment Operator Called!abc
Assignment Operator Called!def
abc
def
*/

分析:

51创建并使用库

如何在vs中设置多个项目,以及如何创建一个能在所有项目中使用的库

51创建并使用库#

先以Game51命名创建一个项目

文件夹查看

再新建一个项目,项目名为Engine

此时


现在确保设置Game51为应用程序

Engine项目配置为静态库

创建文件夹及文件#

show All Files

目录结构

(静态链接库)Engine项目#

//src/Engine.h
#pragma once

namespace engine {
	void PrintMessage();
}
 
//不能这么写,说明:: 被称为作用域解析运算符。它的逻辑
// 是:你只能引用一个已经存在的名字空间。而这里正在定义
//所以不存在
//void engine::PrintMessage();
///src/Engine.cpp
#include "Engine.h"
#include <iostream>

namespace engine {
	void PrintMessage()
	{
		std::cout << "Hello World!" << std::endl;
	}
}

//或者这么写
//void engine::PrintMessage()
//{
//	std::cout << "Hello World!" << std::endl;
//}

(使用静态链接库)Game51项目中#

处理头文件的两个方法#

//Application.cpp

//添加头文件-方法1
//#include "../../Engine/src/Engine.h"

//直接声明-方法0
//namespace engine {
//	void PrintMessage();
//}


int main()
{

	engine::PrintMessage();
}

处理头文件的第3个方法#

右键项目Game51属性修改

50使用动态库

动态链接是什么,以及如何使用

  • 动态链接是在运行时发生的链接,当你实际启动可执行文件时,动态链接库加载时 它并非可执行文件的一部分,此时动态链接库被载入内存。意味着运行时会动态链接另一个库,然后运行应用程序 载入一个额外文件到内存。
    • 这种情况下,可执行文件实际上需要某些库存在(外部文件)。比如windows系统经常出现“缺少某些dll文件无法启动”
    • 可执行文件了解动态链接库,可执行文件是一个独立文件,在运行时加载的独立模块。但也可以加载动态链接库,他会在运行时查找并加载动态库,然后可以获取函数指针/动态库中任何内容
  • 静态链接是在编译时发生的链接,当你编译一个静态库时,将它链接到可执行文件、应用程序或动态库中。即:直接获取静态库的内容,然后把它和其他二进制数据放到一起,静态库实际存在于可执行文件或动态库中,所以编译器、链接器完全清楚这些代码,并会对此进行优化

当我们说“链接到一个动态库”时,实际上是指在创建(生成)那个 DLL 的时候,把静态库的代码塞进去。

补充:动态链接包括“隐式链接” 需要 header (.h) 和 import library (.lib) 和“显示链接” 编译阶段完全不认识这个 DLL,也不需要 .lib 导入库。用法: 运行时使用 Windows API 函数(如 LoadLibrary 和 GetProcAddress)去手动加载。

50使用动态库#

复制Dependencies文件夹到根目录

λ tree Dependencies /f

CHERNOCPP\HELLOWORLD49\DEPENDENCIES
└─GLFW
    ├─include
      └─GLFW
              glfw3.h
              glfw3native.h
    
    └─lib-vc2022
            glfw3.dll
            glfw3.lib
            glfw3dll.lib
            glfw3_mt.lib

静态链接和动态链接都是用同一部分的头文件

目前按ctrl+f7已经能正常编译

glw3dll.lib glfw3dll.lib不存代码,只有地址 ,这是指向glw3.dll的一系列指针,所以我们在运行时无需实际检测所有内容的位置,这两者同时编译很重要,因为在运行时如果尝试使用不同静态库与DLL链接,你可能会遇到函数不匹配,并且函数指针会出现内存地址错误。但是这里是由glfw分发的,所以glw3.dll和glw3dll.lib是同时编译的,而且他们直接相互关联,这两者不能分开。

接下来设置库文件

右键项目并build成功。但是目前如果会提示找不到dll文件

49使用静态库

49使用静态库#

  • 从github库或其他敌法check out代码块,在那个代码库里能直接得到编译所需的一切,并运行应用程序或项目等,无需使用包管理器
  • 倾向于实际保留依赖项的版本,实际解决方案中二进制的版本。实际项目文件夹中有物理二进制文件的副本,或者是源码

自己编译?或者直接链接预构建的二进制文件

  • 建议直接添加另一个项目(依赖项的源代码),然后编译成静态库或动态库
  • 非大型项目,可以直接链接二进制文件(无需源代码)

本篇要处理的是GLFW的库

选择二进制版本 (32位 vs 64位),这取决于你的目标应用程序架构,而不是你的操作系统。

比如现在我要制作一个32位应用程序

//结构

//里面通常是 HTML 格式的 API 手册。
├─docs
  └─html
      └─search
├─include
  └─GLFW

//含义: 针对 MinGW 编译器的版本(通常配合 GCC 使用)。
//用途: 如果你不在 Windows 上用 Visual Studio,而是用 CLion、Code::Blocks 或者直接在命令行用 g++ 编译,你需要链接这个文件夹里的库(通常是 .a 文件而非 .lib)。
├─lib-mingw-w64 
  └─libglfw3.a //(静态库): 如果你用 MinGW 编译器并想进行静态链接
  └─glfw3.dll //(动态链接库): 这是库的实体。 如果你选择动态链接,这个文件必须在运行时放在你的 .exe 旁边
  └─libglfw3dll.a //给编译器看的“目录”。 当你选择动态链接时,你不是直接链接 .dll,而是链接这个 .a 文件。 它告诉编译器:“真正的代码在 glfw3.dll 里,你先编译通过,运行时再去调它。”

//如果你的项目配置为使用通用的 Windows 10/11 SDK 运行时,有时需要专门链接这个版本的库。
├─lib-static-ucrt

//以下是库文件 (.lib),是给 链接器 用的。之所以有这么多,是因为 C++ 的二进制兼容性比较复杂,库的版本必须尽量与你使用的 Visual Studio 版本匹配
├─lib-vc2013
├─lib-vc2015
├─lib-vc2017
├─lib-vc2019
└─lib-vc2022

库由包含目录includes库目录libraries组成

47优化cpp中stdvector的使用

预习

#include <iostream>
#include <string>
#include <vector>

struct Test
{

};
struct Vertex
{
	float x, y, z;

	//有了其他的构造函数,如果需要用到无参构造函数,
	//就必须手动写一个
	Vertex()
	{

	}

	//因为有构造函数了,所以Vertex 结构体不是一个聚合类型(
	// 有构造函数),所以要使用显示构造函数
	// 使 Vertex v = { 1,2,3 }; 编译通过
	Vertex(float x, float y, float z)
	{
		this->x = x;
		this->y = y;
		this->z = z;
	}

	//参数必须是&,原因如下:
	//1. 当你尝试通过 Vertex a = b; 调用复制构造函数时,你需要将 b 传递给参数 other。
	//2. 在 C++ 中,按值传递(Pass by Value)参数本身就会触发一次复制。
	//3. 为了复制 b 到 other,编译器又需要调用复制构造函数。[死循环了]
	//因为main中使用 vector.push({1,2,3}) 需要将临时对象拷贝到vector中,所以这里的参数
	//必须是const
	Vertex(const Vertex& vertex)
	{
		std::cout << "copied constructor handled" << std::endl;
		this->x = vertex.x;
		this->y = vertex.y;
		this->z = vertex.z;
	}
	/*virtual*/ void test()
	{

	}

	~Vertex()
	{
		std::cout << "destructor handled" << std::endl;
	}
};

std::ostream& operator<<(std::ostream& stream, const Vertex& vertex)
{
	stream << vertex.x << "," << vertex.y << "," << vertex.z;
	return stream;
}

//使用引用避免复制整个数组
void Function(const std::vector<Vertex>& vertices)
{

}

int main()
{
	std::vector<Vertex> vertices;
	//vertices.push_back({ 1,2,3 });
	//std::cout << "==0===" << std::endl;
	//vertices.push_back({ 4,5,6 });

	//对上面两行代码优化
	// 1. 预先分配内存,防止搬家
	vertices.reserve(1);
	// 2. 原地构造,防止临时拷贝
	vertices.emplace_back(1, 2, 3);
	std::cout << "==0===" << std::endl;
	//超出容量,会拷贝原来的{1,2,3}到新数组,并且销毁
	// 原数组中的{1,2,3}
	vertices.emplace_back(4, 5, 6);


	std::cout << "==1===" << std::endl;
	Function(vertices);
	std::cout << "==2===" << std::endl;
	vertices.erase(vertices.begin() + 1);
	std::cout << "==3===" << std::endl;
	for (Vertex& v : vertices)
		std::cout << v << std::endl;
	std::cin.get();
}
/*
copied constructor handled //第一个临时对象复制到vector中之后,马上又销毁了
destructor handled
==0===
copied constructor handled //空间不够了,需要把第一个元素复制到新数组中。第二个元素创建临时对象,并复制到新数组中
copied constructor handled
destructor handled  //旧的数组中的第一个元素被销毁了
destructor handled  //第二个元素的临时对象被销毁了
==1===
==2===
destructor handled
==3===
1,2,3
*/

/*优化后不需要把临时对象复制到vector中,也不需要销毁临时对象了
==1===
==2===
destructor handled
==3===
1,2,3
*/

了解环境是优化过程中最重要的事情之一

46动态数组

  • 动态数组,特别是讨论标准向量类
  • 要习惯CPP的标准库(标准模版库)
  • 标准模版库包含了各种容器、迭代器、算法、仿函数

  • 容器的数据类型由程序员自己决定
  • cpp提供了一个Vector的类,在std命名空间中,可以调整大小,也可以不指定初始大小
  • 本系列会重写C++中的数据结构,优化后比标准模版库中的快很多

std::vector 的实际工作方式:

当超出实际的分配大小时。当创建一个向量,可能会先分配10个元素大小的空间,当超出这个大小时,会在内存中创建一个比原来更大的数组,并把所有内容复制过去,然后删除旧数组。这样就有了一个更大存储空间的新数组

46动态数组#

struct Test
{

};
struct Vertex 
{  
	float x, y, z;
	/*virtual*/ void test()
	{

	}
};

//main()中
Vertex v = { 1,2,3 };

聚合初始化(Aggregate Initialization),Vertex 结构体是一个聚合类型(Aggregate Type)。在 C++ 中,如果一个类或结构体满足以下条件:

  • 没有显式定义的构造函数。
  • 成员变量是公有的(public)。
  • 没有基类(没有继承)。
  • 没有虚函数。

那么你可以使用大括号 {} 直接按顺序为成员变量赋值。

vector的基本使用#

#include <iostream>
#include <string>
#include <vector>

struct Test
{

};
struct Vertex
{
	float x, y, z;

	//有了其他的构造函数,如果需要用到无参构造函数,
	//就必须手动写一个
	Vertex()
	{

	}

	//因为有构造函数了,所以Vertex 结构体不是一个聚合类型(
	// 有构造函数),所以要使用显示构造函数
	// 使 Vertex v = { 1,2,3 }; 编译通过
	Vertex(float x, float y, float z)
	{
		this->x = x;
		this->y = y;
		this->z = z;
	}

	//参数必须是&,原因如下:
	//1. 当你尝试通过 Vertex a = b; 调用复制构造函数时,你需要将 b 传递给参数 other。
	//2. 在 C++ 中,按值传递(Pass by Value)参数本身就会触发一次复制。
	//3. 为了复制 b 到 other,编译器又需要调用复制构造函数。[死循环了]
	//因为main中使用 vector.push({1,2,3}) 需要将临时对象拷贝到vector中,所以这里的参数
	//必须是const
	Vertex(const Vertex& vertex)
	{
		std::cout << "copied constructor handled" << std::endl;
		this->x = vertex.x;
		this->y = vertex.y;
		this->z = vertex.z;
	}
	/*virtual*/ void test()
	{

	}

	~Vertex()
	{
		std::cout << "destructor handled" << std::endl;
	}
};

std::ostream& operator<<(std::ostream& stream, const Vertex& vertex)
{
	stream << vertex.x << "," << vertex.y << "," << vertex.z;
	return stream;
}

int main()
{
	Vertex vertices1[5];
	Vertex* vertices2 = new Vertex[5];

	std::vector<int> myarray;
	//存储Vertex对象,则动态数组的内存是连续的,
	//其中的Vertext对象会按行排列
	//问题是:实际要调整向量大小时得复制所有(类)数据,但内存连续性
	// 带来的读取速度提升通常远超扩容时的开销
	std::vector<Vertex> vertices;
	//存储Vertext指针,实际要调整向量大小是只是复制指针地址(一个数字)
	//,即实际数据的内存地址
	std::vector<Vertex*> vertices4;

	std::cout << "---1---" << std::endl;
	Vertex v = { 1,2,3 };
	//这里演示vertices的使用
	std::cout << "---2---" << std::endl;
	vertices.push_back({ 1,2,3 });
	std::cout << "---3---" << std::endl;
	vertices.push_back({ 4,5,6 });

	std::cout << "---4---" << std::endl;
	for (int i = 0; i < vertices.size(); i++)
	{
		//c++对[]进行了重载
		std::cout << vertices[i] << std::endl;
	}
	std::cout << "---5---" << std::endl;
	//这里会把每个Vertex复制到v
	for (Vertex v : vertices)
		std::cout << v << std::endl;
	std::cout << "---6---" << std::endl;
	//避免复制
	for (Vertex& v : vertices)
		std::cout << v << std::endl;

	//clear() 会移除容器中的所有元素,但通常不一定会立即释放底层内存(容量不变),它只是销毁现有的对象。
	vertices.clear();

	std::cin.get();
}

解释一下vertices.push_back({1, 2, 3});#

当你执行 vertices.push_back({1, 2, 3}); 时,逻辑确实是:在 vector 内部的内存中,通过调用复制构造函数来构造一个属于 vector 自己的对象。

999_1柏杨跋

柏杨跋#

《资治通鉴》,司马光本是写给帝王看的,希望加强他们的统治的技术,可是事实上帝王并没有从中获益,获益的反而倒是平民,当我们发现政治舞台上的节目和《资治通鉴》上的人物举动重叠的时候,我们就可以预测它下一步的剧情。

然而,教训不是历史的主要功能,中国的实用主义文化,总是使艺术和学术作品、跟功利结合。事实上,历史的主要功能在于使我们认同我们所来自的世界,而有一种知道身世后的归属感。没有归属感的心灵,不能延续文化的薪传,而成为太空中的浮飘物。

《资治通鉴》记载太多的悲惨和丑恶,但《圣经》是犹太人的《资治通鉴》,里面有同样多的悲惨和丑恶,美国历史上有水牢、吊人树,欧洲历史上有伦敦塔、巴士底监狱,都不影响他们的后裔以他们的国家和民族为荣,中国人亦然,《资治通鉴》使我们归属于我们自己的文化,使我们对未来的发展,产生一种神圣使命,要她再没有污点、只有荣耀,使明天更有意义。

000_3导读

《资治通鉴》从公元前四〇三年写起,因为这一年发生了巨变:晋国的三家大臣——韩虔 Qián 、赵籍、魏斯瓜分了晋国,周王朝也不得不予以承认。韩、赵、魏三国的诞生,标志着一个新的时代——战国时代开始。

《战国时代》叙述了从“三家分晋”开始的一百余年的巨变。这是一个重新洗牌的时代,各国靠实力来说话。在一些国家发生的变革特别引人注目:吴起在楚国厉行政治革新,秦国采用公孙鞅的主张大兴变法,赵国厉行胡服骑射。变革的创议者如吴起、公孙鞅虽然因触犯贵族们的利益而惨死,却使这些国家因变革而国富兵强,在各国纷纷扰扰的讨伐和吞并中占了上风。齐、楚、燕、韩、赵、魏、秦,七雄并立,构成了这一时代的政治版图。

编者(柏杨)

000_2司马光进呈《资治通鉴》表

臣光言:先奉敕編集歷代君臣事迹,又奉聖旨賜名《資治通鑑》,今已了畢者。

臣司马光言:

先前,接奉圣旨,要我编纂历代君臣事迹。不久,再接奉圣旨,赐名《资治通鉴》。现在,全书已完全定稿。

伏念臣性識愚魯,學術荒疏,凡百事爲,皆出人下。獨於前史,粗嘗盡心,自幼至老,嗜之不厭。每患遷、固以來,文字繁多,自布衣之士,讀之不徧,況於人主,日有萬機,何暇周覽!臣常不自揆,欲删削宂長,舉撮機要,專取關國家盛衰,繫生民休戚,善可爲法,惡可爲戒者,爲編年一書,使先後有倫,精粗不雜,私家力薄,無由可成。

我性情愚昧而且鲁莽,学术更是荒疏,所做的事,都在别人之下。唯独对于历史,心有所爱,从幼到老,嗜好不倦。深深地感觉到,自从司马迁、班固以来,史籍越来越多,普通人有的是时间,还读不完,更何况高高在上的君王,日理万机,哪有闲暇?我常怀一种抱负,打算加以整理,删除多余的废话,摘取其中的精华,专门收集有关国家兴衰,人民悲欢,善可以为法,恶可以为戒的政治行为,编著一部编年史。使先后顺序,明确呈现,内容篇幅,繁简适当。只因为私人力量单薄,无法着手。

伏遇英宗皇帝,資睿智之性,敷文明之治,思歷覽古事,用恢張大猷,爰詔下臣,俾之編集。臣夙昔所願,一朝獲伸,踊躍奉承,惟懼不稱。先帝仍命自選辟官屬,於崇文院置局,許借龍圖、天章閣、三館、祕閣書籍,賜以御府筆墨繒帛及御前錢以供果餌,以內臣爲承受,眷遇之榮,近臣莫及。不幸書未進御,先帝違棄羣臣。陛下紹膺大統,欽承先志,寵以冠序,錫之嘉名,每開經筵,常令進讀。臣雖頑愚,荷兩朝知待如此其厚,隕身喪元,未足報塞,苟智力所及,豈敢有遺!會差知永興軍,以衰疾不任治劇,乞就宂官。陛下俯從所欲,曲賜容養,差判西京留司御史臺及提舉嵩山崇福宮,前後六任,仍聽以書局自隨,給之祿秩,不責職業。臣旣無他事,得以研精極慮,窮竭所有,日力不足,繼之以夜。徧閱舊史,旁采小說,簡牘盈積,浩如煙海,抉擿幽隱,校計豪釐。上起戰國,下終五代,凡一千三百六十二年,修成二百九十四卷。又略舉事目,年經國緯,以備檢尋,爲《目錄》三十卷。又參考羣書,評其同異,俾歸一塗,爲《考異》三十卷。合三百五十四卷。自治平開局,迨今始成,歲月淹久,其間抵牾,不敢自保,罪負之重,固無所逃。臣光誠惶誠懼,頓首頓首。


称呼类型具体名称备注
庙号宋英宗文中提到的“英宗皇帝”。
姓名赵曙文中开头提到的名字。
曾用名赵宗实文中括号里提到的名字(他过继给仁宗前叫这个)。
排序北宋第五位皇帝承接宋仁宗,开启了宋神宗时代。

幸而遇到英宗皇帝(宋王朝五任帝赵曙),聪明睿智,关心文化推展,想了解古时政事,借此作为制定国家大计方针的根据。特地下令,教我着手编纂。往日的愿望,忽然可以发挥,欢欣鼓舞,不能自已。唯一恐惧的是,才疏学浅,难以胜任。先帝(五任帝赵宗实)又命我自己物色任用助手,在崇文院内,设立编辑局,准许向龙图阁、天章阁、“三馆”(昭文馆、集贤馆、国史馆)以及秘阁等图书馆,借用图书。并发给御用的笔墨纸砚,更特别犒赏,购买水果点心。并指定宦官充当联络官,直接可以奏报先帝。受恩之深,受宠之隆,近代从来没有。不幸书还没有进呈,先帝竟行去世。陛下(六任帝赵顼)继位大统,也继承遗志,颁赐序文,亲为本书命名。御前讲座时,也常命我宣读。我虽然愚昧,但受到两任皇上如此厚待,即令杀身枭首,也不能报答万一。只要能力够用,岂敢有丝毫惰怠?那时,政府派我代理永兴(陕西省西安市)战区司令官(知永兴军),因身体衰弱,又患病未痊,不能从事繁重工作,请求改调其他官职。陛下顾念下情,答应我的请求,命我担任西京(河南省洛阳市)留守政府监察总监(判西京留司御史台),兼任西京嵩山崇福宫管理官(提举西京嵩山崇福宫)。前后六次调动职务,都准允编辑局跟我一同迁移。并且只发经费,从不规定按时缴出成绩。我既没有其他重大事务,就投入全部精力,精细研究,竭尽心力。白天不够使用,继之以黑夜。不但选录正史,还从旁采及野史(小说)、书信和文件,堆积得好像大海。我们在最隐秘处发掘历史真相,对每一个字都校正它是否错误。上起战国,下至五代,凡一千三百六十二年,共二百九十四卷。另外,再编索引,以年月为纵的轨迹,以事件为横的叙述,便于读者查考,命名《目录》,凡三十卷。再另外,参考各种图书,考证它们的异同,说明真伪,再成《考异》三十卷。——总共三百五十四卷。回溯过去,自本世纪(十一世纪)六〇年代开始着手,到今天才算完成,悠悠岁月,中间受到政局影响,我自己都不敢相信能够平安,负罪至重,不容逃避。

重念臣違離闕庭,十有五年,雖身處于外,區區之心,朝夕寤寐,何嘗不在陛下之左右!顧以駑蹇,無施而可,是以專事鉛槧,用酬大恩,庶竭涓塵,少裨海嶽。臣今骸骨癯瘁,目視昏近,齒牙無幾,神識衰耗,目前所爲,旋踵遺忘。臣之精力,盡於此書。伏望陛下寬其妄作之誅,察其願忠之意,以清閒之宴,時賜有覽,監前世之興衰,考當今之得失,嘉善矜惡,取得捨非,足以懋稽古之盛德,躋無前之至治。俾四海羣生,咸蒙其福,則臣雖委骨九泉,志願永畢矣!

臣司马光,诚惶诚恐,顿首顿首。敬请陛下垂念我远离中央,已十有五年,虽然身在外地,但区区之心,无论早上或黄昏,无论清醒或睡梦,何尝不在陛下左右。只以天性拙笨,无从效力,是以专门从事文字工作,报答皇恩,只求点滴之水和微粒之尘,以增加大海之深和大山之高。我现在骨骸憔悴,双目近视,牙齿几乎全部脱落,精神耗损枯竭。眼前办的事,一转身就都忘掉,残余精力,在此书上全部耗尽。敬请陛下(六任帝赵顼)宽恕我因妄自著作而应诛杀的重罪,俯察我一念之忠,在清闲休息的时候,顺手翻阅。==参考前代王朝的兴衰,考查当今政治措施的得失,嘉奖善良,排除罪恶,坚持正义,改正错误,就足可以追踪古代的盛世,使国家迈入以前从没有过的太平境界。==四海之内的苍生,都蒙受到福祉。那么,我虽葬身黄泉之下,平生志愿也已得到回报。

謹奉表陳進以聞。臣光誠惶誠懼,頓首頓首,謹言。

端明殿學士兼翰林侍讀學士太中大夫提舉西京嵩山崇福

宮上柱國河內郡開國公食邑二千六百戶食實封一千戶臣

司馬光 上表

元豐七年十一月進呈

端明殿学士兼翰林侍读学士、太中大夫、提举西京嵩山崇福宫、上柱国、河内郡开国公、食邑二千六百户、实封一千户臣司马光上表。

一〇八四年十一月进呈

檢閱文字承事郞 司馬康

同修奉議郞 范祖禹

同修祕書丞 劉 恕

同修尚書屯田員外郞充集賢校理 劉 攽

編集端明殿學士兼翰林侍讀學士太中大夫 司馬光


獎諭詔書 敕司馬光:修《資治通鑑》成事。

史學之廢久矣,紀次無法,論議不明,豈足以懲勸,明久遠哉!卿博學多聞,貫穿今古,上但晚周,下迄五代,發揮綴緝,成一家之書,褒貶去取,有所據依。省閱以還,良深嘉歎!今賜卿銀絹、對衣、腰帶、鞍轡馬,具如別錄,至可領也。故茲獎諭。想宜知悉。

冬寒,卿比平安好。遣書,指不多及。  十五日。

元豐八年九月十七日准尚書省劄子奉聖旨重行校定

元祐元年十月十四日奉聖旨下杭州鏤板