我的学记|刘航宇的博客 - 编程&脚本笔记
编译一个C语言程序是一个多阶段的过程。从总体上看，这个过程可以分成四个独立的阶段。预处理、编译、汇编和连接。在这篇文章中，我将逐一介绍编译下列C程序的四个阶段。/* * "Hello, World!": A classic. */ #include <stdio.h> int main(void) { puts("Hello, World!"); return 0; }预处理编译的第一个阶段称为预处理。在这个阶段，以#字符开头的行被预处理器解释为预处理器命令。这些命令形成一种简单的宏语言，有自己的语法和语义。这种语言通过提供内联文件、定义宏和有条件地省略代码的功能，来减少源代码的重复性。在解释命令之前，预处理器会做一些初始处理。这包括连接续行（以 \ 结尾的行）和剥离注释。要打印预处理阶段的结果，请向gcc传递-E选项。 gcc -E hello_world.c 考虑到上面的 "Hello, World!"的例子，预处理器将产生stdio.h头文件的内容和hello_world.c文件的内容，并将其前面的注释剥离出来。编译编译的第二个阶段被称为编译，令人困惑。在这个阶段，预处理过的代码被翻译成目标处理器架构特有的汇编指令。这些形成了一种中间的人类可读语言。这一步骤的存在允许C代码包含内联汇编指令，并允许使用不同的汇编器。一些编译器也支持使用集成汇编器，在这种情况下，编译阶段直接生成机器代码，避免了生成中间汇编指令和调用汇编器的开销。要保存编译阶段的结果，可以向gcc传递-S选项。 gcc -S hello_world.c 这将创建一个名为hello_world.s的文件，包含生成的汇编指令。汇编在这个阶段，汇编器被用来将汇编指令翻译成目标代码。输出包括目标处理器要运行的实际指令。要保存汇编阶段的结果，请向gcc传递-c选项。 gcc -c hello_world.c运行上述命令将创建一个名为hello_world.o的文件，包含程序的目标代码。这个文件的内容是二进制格式，可以用运行命令hexdump或od来检查。hexdump hello_world.o od -c hello_world.oLinux中的od(octal dump)命令用于转换输入内容为八进制。Hexdump是一个命令行工具，用于以各种方式显示文件的原始内容，包括十六进制，可用于Linux、FreeBDS、OS X和其他平台。Hexdump不是传统Unix系统或GNU命令的一部分。链接汇编阶段产生的目标代码是由处理器能够理解的机器指令组成的，但程序的某些部分是不符合顺序的或缺失的。为了产生一个可执行的程序，现有的部分必须被重新排列，并把缺失的部分补上。这个过程被称为链接。链接器将安排目标代码的各个部分，使某些部分的功能能够成功地调用其他部分的功能。它还将添加包含程序所使用的库函数指令的片段。在 "Hello，world！"程序的例子中，链接器将添加puts函数的对象代码。这一阶段的结果是最终的可执行程序。当不使用选项运行时，gcc 将把这个文件命名为 a.out。如果要给文件命名，请向 gcc 传递 -o 选项。gcc -o hello_world hello_world.c
前言那么究竟什么是面向对象呢？面向过程面向对象面向对象的三大特性封装继承多态面向对象的五大基本原则单一职责原则（Single-Responsibility Principle）开放封闭原则（Open-Closed principle）里氏替换原则（Liskov-Substitution Principle）依赖倒置原则（Dependecy-Inversion Principle）接口隔离原则（Interface-Segregation Principle）面向对象编程的用途面向对象编程的应用领域前言在刚接触java、C++、Python语言的时候，就知道这是一门面向对象的语言。学不好java的原因找到了，面向对象的语言，没有对象怎么学 ::(狗头) 那么究竟什么是面向对象呢？面向对象，Object Oriented Programming，简称为OOP。说到面向对象，不得不提一嘴面向过程。面向过程面向过程是一种自顶而下的编程模式。把问题分解成一个一个步骤，每个步骤用函数实现，依次调用即可。举个生活中的例子，假如你想吃红烧肉，你需要买肉，买调料，洗肉，切肉，烧肉，装盘。需要我们具体每一步去实现，每个步骤相互协调，最终盛出来的才是正宗好吃的红烧肉。面向对象面向对象就是将问题分解成一个一个步骤，对每个步骤进行相应的抽象，形成对象，通过不同对象之间的调用，组合解决问题。还是你想吃红烧肉这个例子，不过这次不同的是你发现了一家餐馆里有红烧肉这道菜，你要做的只是去点菜，就可以吃到红烧肉。针不戳，针不戳，面向对象针不戳，你不用再去关心红烧肉繁琐的制作流程，就能吃到美味的红烧肉。我们接着往下看面向对象的三大特性封装封装就是把客观的事物封装成抽象的类，并且类可以把自己的数据和方法只让可信的类或者对象操作，对不可信的类进行信息的隐藏。简单的说就是：封装使对象的设计者与对象的使用者分开，使用者只要知道对象可以做什么就可以了，不需要知道具体是怎么实现的。封装可以有助于提高类和系统的安全性这里有点像FPGA的IP了，电子人懂！继承当多个类中存在相同属性和行为时，将这些内容就可以抽取到一个单独的类中，使得多个类无需再定义这些属性和行为，只需继承那个类即可。通过继承创建的新类称为“子类”或“派生类”，被继承的类称为“基类”、“父类”或“超类”多态多态同一个行为具有多个不同表现形式或形态的能力。是指一个类实例（对象）的相同方法在不同情形有不同表现形式。多态机制使具有不同内部结构的对象可以共享相同的外部接口。这意味着，虽然针对不同对象的具体操作不同，但通过一个公共的类，它们（那些操作）可以通过相同的方式予以调用。菜鸟教程中的例子就很形象{message type="success" content="现实中，比如我们按下 F1 键这个动作：如果当前在 Flash 界面下弹出的就是 AS 3 的帮助文档；如果当前在 Word 下弹出的就是 Word 帮助；在 Windows 下弹出的就是 Windows 帮助和支持。同一个事件发生在不同的对象上会产生不同的结果。"/}面向对象的五大基本原则单一职责原则（Single-Responsibility Principle）简而言之，就是一个类最好只有一个能引起变化的原因，只做一件事，单一职责原则可以看做是低耦合高内聚思想的延伸，提高高内聚来减少引起变化的原因。开放封闭原则（Open-Closed principle）简而言之，就是软件实体应该是可扩展的，但是不可修改。因为修改程序有可能会对原来的程序造成错误。即对扩展开放，对修改封闭里氏替换原则（Liskov-Substitution Principle）简而言之，就是子类一定可以替换父类，子类包含其基类（父类）的功能依赖倒置原则（Dependecy-Inversion Principle）高层次的模块不应该依赖于低层次的模块，他们都应该依赖于抽象。抽象不应该依赖于具体实现，具体实现应该依赖于抽象。依赖于抽象即对接口编程，不要对实现编程接口隔离原则（Interface-Segregation Principle）简而言之，就是使用多个小的专门的接口，而不要使用一个大的总接口。例如将一个房子再分割成卧室，厨房，厕所等等，而不是将所有功能放在一起面向对象编程的用途使用封装（信息隐藏）可以对外部隐藏数据使用继承可以重用代码使用多态可以重载操作符/方法/函数，即相同的函数名或操作符名称可用于多种任务数据抽象可以用抽象实现项目易于迁移（可以从小项目转换成大项目）同一项目分工软件复杂性可控面向对象编程的应用领域人工智能与专家系统企业级应用神经网络与并行编程办公自动化系统
华为C++算法-识别有效的IP地址和掩码并进行分类统计
问题注意：输入描述：输出描述：示例需要注意的细节思路具体实现代码问题请解析IP地址和对应的掩码，进行分类识别。要求按照A/B/C/D/E类地址归类，不合法的地址和掩码单独归类。所有的IP地址划分为 A,B,C,D,E五类A类地址从1.0.0.0到126.255.255.255;B类地址从128.0.0.0到191.255.255.255;C类地址从192.0.0.0到223.255.255.255;D类地址从224.0.0.0到239.255.255.255；E类地址从240.0.0.0到255.255.255.255私网IP范围是：从10.0.0.0到10.255.255.255从172.16.0.0到172.31.255.255从192.168.0.0到192.168.255.255子网掩码为二进制下前面是连续的1，然后全是0。（例如：255.255.255.32就是一个非法的掩码）（注意二进制下全是1或者全是0均为非法子网掩码）注意：类似于【0...】和【127...】的IP地址不属于上述输入的任意一类，也不属于不合法ip地址，计数时请忽略私有IP地址和A,B,C,D,E类地址是不冲突的输入描述：多行字符串。每行一个IP地址和掩码，用~隔开。输出描述：统计A、B、C、D、E、错误IP地址或错误掩码、私有IP的个数，之间以空格隔开。示例{message type="success" content="输入：10.70.44.68~255.254.255.01.0.0.1~255.0.0.0192.168.0.2~255.255.255.019..0.~255.255.255.0输出：1 0 1 0 0 2 1说明：10.70.44.68~255.254.255.0的子网掩码非法，19..0.~255.255.255.0的IP地址非法，所以错误IP地址或错误掩码的计数为2；1.0.0.1~255.0.0.0是无误的A类地址；192.168.0.2~255.255.255.0是无误的C类地址且是私有IP；所以最终的结果为1 0 1 0 0 2 1 "/}示例2{message type="success" content="输入：0.201.56.50~255.255.111.255127.201.56.50~255.255.111.255输出：0 0 0 0 0 0 0说明：类似于【0...】和【127...】的IP地址不属于上述输入的任意一类，也不属于不合法ip地址，计数时请忽略 "/}需要注意的细节类似于【0...】和【127...】的IP地址不属于上述输入的任意一类，也不属于不合法ip地址，计数时可以忽略私有IP地址和A,B,C,D,E类地址是不冲突的，也就是说需要同时+1如果子网掩码是非法的，则不再需要查看IP地址全零【0.0.0.0】或者全一【255.255.255.255】的子网掩码也是非法的思路按行读取输入，根据字符‘~’ 将IP地址与子网掩码分开查看子网掩码是否合法。合法，则继续检查IP地址非法，则相应统计项+1，继续下一行的读入查看IP地址是否合法合法，查看IP地址属于哪一类，是否是私有ip地址；相应统计项+1非法，相应统计项+1具体实现判断IP地址是否合法，如果满足下列条件之一即为非法地址数字段数不为4存在空段，即【192..1.0】这种某个段的数字大于255判断子网掩码是否合法，如果满足下列条件之一即为非法掩码不是一个合格的IP地址在二进制下，不满足前面连续是1，然后全是0在二进制下，全为0或全为1如何判断一个掩码地址是不是满足前面连续是1，然后全是0？将掩码地址转换为32位无符号整型，假设这个数为b。如果此时b为0，则为非法掩码将b按位取反后+1。如果此时b为1，则b原来是二进制全1，非法掩码如果b和b-1做按位与运算后为0，则说明是合法掩码，否则为非法掩码代码注意getline函数可以指定分割字符串的字符// 引入输入输出流、字符串、字符串流和向量等头文件 #include<iostream> #include<string> #include<sstream> #include<vector> // 使用标准命名空间 using namespace std; // 定义一个函数，判断一个字符串是否是合法的IP地址 bool judge_ip(string ip){ // 定义一个整数变量，记录IP地址的段数 int j = 0; // 定义一个字符串流对象，用于分割IP地址 istringstream iss(ip); // 定义一个字符串变量，用于存储IP地址的每一段 string seg; // 使用循环，以'.'为分隔符，获取IP地址的每一段 while(getline(iss,seg,'.')) // 如果段数加一大于4，或者该段为空，或者该段的数值大于255，说明不是合法的IP地址，返回false if(++j > 4 || seg.empty() || stoi(seg) > 255) return false; // 如果循环结束后，段数等于4，说明是合法的IP地址，返回true return j == 4; } // 定义一个函数，判断一个字符串是否是私有的IP地址 bool is_private(string ip){ // 定义一个字符串流对象，用于分割IP地址 istringstream iss(ip); // 定义一个字符串变量，用于存储IP地址的每一段 string seg; // 定义一个整数向量，用于存储IP地址的每一段的数值 vector<int> v; // 使用循环，以'.'为分隔符，获取IP地址的每一段，并将其转换为整数，存入向量中 while(getline(iss,seg,'.')) v.push_back(stoi(seg)); // 如果IP地址的第一段等于10，说明是私有的IP地址，返回true if(v[0] == 10) return true; // 如果IP地址的第一段等于172，并且第二段在16到31之间，说明是私有的IP地址，返回true if(v[0] == 172 && (v[1] >= 16 && v[1] <= 31)) return true; // 如果IP地址的第一段等于192，并且第二段等于168，说明是私有的IP地址，返回true if(v[0] == 192 && v[1] == 168) return true; // 如果以上条件都不满足，说明不是私有的IP地址，返回false return false; } // 定义一个函数，判断一个字符串是否是合法的子网掩码 bool is_mask(string ip){ // 定义一个字符串流对象，用于分割IP地址 istringstream iss(ip); // 定义一个字符串变量，用于存储IP地址的每一段 string seg; // 定义一个无符号整数变量，用于存储IP地址的二进制表示 unsigned b = 0; // 使用循环，以'.'为分隔符，获取IP地址的每一段，并将其转换为整数，左移8位后与b进行按位或运算，得到IP地址的二进制表示 while(getline(iss,seg,'.')) b = (b << 8) + stoi(seg); // 如果b等于0，说明不是合法的子网掩码，返回false if(!b) return false; // 将b按位取反后加一，得到b的补码 b = ~b + 1; // 如果b等于1，说明不是合法的子网掩码，返回false if(b == 1) return false; // 如果b与b减一进行按位与运算，结果等于0，说明b只有一个1，说明是合法的子网掩码，返回true if((b & (b-1)) == 0) return true; // 如果以上条件都不满足，说明不是合法的子网掩码，返回false return false; } // 定义主函数 int main(){ // 定义一个字符串变量，用于存储输入的IP地址和子网掩码 string input; // 定义七个整数变量，用于统计A、B、C、D、E类地址、错误地址和私有地址的个数 int a = 0,b = 0,c = 0,d = 0,e = 0,err = 0,p = 0; // 使用循环，读取输入的IP地址和子网掩码，直到输入结束 while(cin >> input){ // 定义一个字符串流对象，用于分割IP地址和子网掩码 istringstream is(input); // 定义一个字符串变量，用于存储IP地址或子网掩码 string add; // 定义一个字符串向量，用于存储IP地址和子网掩码 vector<string> v; // 使用循环，以'~'为分隔符，获取IP地址和子网掩码，并存入向量中 while(getline(is,add,'~')) v.push_back(add); // 如果IP地址或子网掩码不合法，错误地址的个数加一 if(!judge_ip(v[1]) || !is_mask(v[1])) err++; else{ // 如果IP地址不合法，错误地址的个数加一 if(!judge_ip(v[0])) err++; else{ // 获取IP地址的第一段的数值 int first = stoi(v[0].substr(0,v[0].find_first_of('.'))); // 如果IP地址是私有的，私有地址的个数加一 if(is_private(v[0])) p++; // 根据IP地址的第一段的数值，判断IP地址的类别，并相应的类别的个数加一 if(first > 0 && first <127) a++; else if(first > 127 && first <192) b++; else if(first > 191 && first <224) c++; else if(first > 223 && first <240) d++; else if(first > 239 && first <256) e++; } } } // 输出A、B、C、D、E类地址、错误地址和私有地址的个数 cout << a << " " << b << " " << c << " " << d << " " << e << " " << err << " " << p << endl; // 返回0，表示程序正常结束 return 0; }
描述基础数据结构知识回顾题解C++篇题解Python篇描述给定一个单链表的头结点pHead(该头节点是有值的，比如在下图，它的val是1)，长度为n，反转该链表后，返回新链表的表头。数据范围： 0≤n≤1000要求：空间复杂度 O(1) ，时间复杂度 O(n) 。如当输入链表{1,2,3}时，经反转后，原链表变为{3,2,1}，所以对应的输出为{3,2,1}。以上转换过程如下图所示：基础数据结构知识回顾空间复杂度 O (1) 表示算法执行所需要的临时空间不随着某个变量 n 的大小而变化，即此算法空间复杂度为一个常量，可表示为 O (1)。例如，下面的代码中，变量 i、j、m 所分配的空间都不随着 n 的变化而变化，因此它的空间复杂度是 O (1)。int i = 1; int j = 2; ++i; j++; int m = i + j;时间复杂度 O (n) 表示算法执行的时间与 n 成正比，即此算法时间复杂度为线性阶，可表示为 O (n)。例如，下面的代码中，for 循环里面的代码会执行 n 遍，因此它消耗的时间是随着 n 的变化而变化的，因此这类代码都可以用 O (n) 来表示它的时间复杂度。for (i=1; i<=n; ++i) { j = i; j++; }题解C++篇可以先用一个vector将单链表的指针都存起来，然后再构造链表。此方法简单易懂，代码好些。// 定义一个Solution类 class Solution { public: // 定义一个函数，接收一个链表的头节点指针，返回一个反转后的链表的头节点指针 ListNode* ReverseList(ListNode* pHead) { // 如果头节点指针为空，直接返回空指针 if (!pHead) return nullptr; // 定义一个vector，用于存储链表中的每个节点指针 vector<ListNode*> v; // 遍历链表，将每个节点指针放入vector中 while (pHead) { v.push_back(pHead); pHead = pHead->next; } // 反转vector，也可以逆向遍历 reverse(v.begin(), v.end()); // 取出vector中的第一个元素，作为反转后的链表的头节点指针 ListNode *head = v[0]; // 定义一个当前节点指针，初始化为头节点指针 ListNode *cur = head; // 从第二个元素开始遍历vector，构造反转后的链表 for (int i=1; i<v.size(); ++i) { // 当前节点的下一个指针指向下一个节点 cur->next = v[i]; // 当前节点后移 cur = cur->next; } // 切记最后一个节点的下一个指针指向nullptr cur->next = nullptr; // 返回反转后的链表的头节点指针 return head; } };初始化：3个指针1）pre指针指向已经反转好的链表的最后一个节点，最开始没有反转，所以指向nullptr2）cur指针指向待反转链表的第一个节点，最开始第一个节点待反转，所以指向head3）nex指针指向待反转链表的第二个节点，目的是保存链表，因为cur改变指向后，后面的链表则失效了，所以需要保存接下来，循环执行以下三个操作1）nex = cur->next, 保存作用2）cur->next = pre 未反转链表的第一个节点的下个指针指向已反转链表的最后一个节点3）pre = cur， cur = nex; 指针后移，操作下一个未反转链表的第一个节点循环条件，当然是cur != nullptr循环结束后，cur当然为nullptr，所以返回pre，即为反转后的头结点这里以1->2->3->4->5 举例：// 定义一个Solution类 class Solution { public: // 定义一个函数，接收一个链表的头节点指针，返回一个反转后的链表的头节点指针 ListNode* ReverseList(ListNode* pHead) { // 定义一个前驱节点指针，初始化为nullptr ListNode *pre = nullptr; // 定义一个当前节点指针，初始化为头节点指针 ListNode *cur = pHead; // 定义一个后继节点指针，初始化为nullptr ListNode *nex = nullptr; // 遍历链表，反转每个节点的指向 while (cur) { // 记录当前节点的下一个节点 nex = cur->next; // 将当前节点的下一个指针指向前驱节点 cur->next = pre; // 将前驱节点更新为当前节点 pre = cur; // 将当前节点更新为后继节点 cur = nex; } // 返回反转后的链表的头节点指针，即原链表的尾节点指针 return pre; } };题解Python篇假设 链表为 1->2->3->4->null 空就是链表的尾obj： 4->3->2->1->null那么逻辑是首先设定待反转链表的尾 pre = nonehead 代表一个动态的表头 逐步取下一次链表的值然后利用temp保存 head.next 第一次迭代head为1 temp 为2 原始链表中是1->2现在我们需要翻转 即 令head.next = pre 实现 1->none但此时链表切断了 变成了 1->none 2->3->4所以我们要移动指针，另pre = head 也就是pre从none 变成1 下一次即可完成2->1的链接此外另head = next 也就是说 把指针移动到后面仍然链接的链表上这样执行下一次循环 则实现 把2->3 转变为 2->1->none然后再次迭代直到最后一次 head 变成了none 而pre变成了4 则pre是新的链表的表头完成翻转# -*- coding:utf-8 -*- # 定义一个ListNode类，表示链表中的节点 # class ListNode: # def __init__(self, x): # self.val = x # 节点的值 # self.next = None # 节点的下一个指针 # 定义一个Solution类，用于解决问题 class Solution: # 定义一个函数，接收一个链表的头节点，返回一个反转后的链表的头节点 def ReverseList(self, pHead): # write code here pre = None # 定义一个前驱节点，初始化为None head = pHead # 定义一个当前节点，初始化为头节点 while head: # 遍历链表，反转每个节点的指向 temp = head.next # 记录当前节点的下一个节点 head.next = pre # 将当前节点的下一个指针指向前驱节点 pre = head # 将前驱节点更新为当前节点 head = temp # 将当前节点更新为下一个节点 return pre # 返回反转后的链表的头节点，即原链表的尾节点
意义题目示例解答1.char指针，按字节替换2.利用union联合体共用内存空间特性，使用char数组来改变3. 使用按位与运算保留以获取每个字节，然后按位左移到正确位置并拼接4. 使用预定义好的宏函数意义大端小端转化对嵌入式系统有意义，因为不同的处理器或者通信协议可能采用不同的字节序来存储或者传输数据。字节序是指一个多字节数据在内存中的存放顺序，它有两种主要的形式：大端：最高有效位（MSB）存放在最低的内存地址，最低有效位（LSB）存放在最高的内存地址。小端：最低有效位（LSB）存放在最低的内存地址，最高有效位（MSB）存放在最高的内存地址。例如，一个32位的整数0x12345678，在大端系统中，它的内存布局是：而在小端系统中，它的内存布局是：如果一个嵌入式系统需要和不同字节序的设备或者网络进行交互，就需要进行字节序的转换，否则会导致数据错误或者通信失败。例如，TCP/IP协议族中的所有层都采用大端字节序来表示数据包头中的16位或32位的值，如IP地址、包长、校验和等。如果一个嵌入式系统使用小端字节序的处理器，并且想要建立一个TCP连接，就需要将IP地址等信息从小端转换为大端再发送出去，否则对方无法正确解析。题目输入一个数字n，假设它是以小端模式保存在机器的，请将其转换为大端方式保存时的值。示例输入：1返回值：16777216解答1.char指针，按字节替换/* * @param n int整型 * @return int整型 */ int convert(int n ) { // write code here int tmp = 0x00000000; //开辟新的int空间用于接收转化结果 unsigned char *p = &tmp, *q = &n; p[0] = q[3]; p[1] = q[2]; p[2] = q[1]; p[3] = q[0]; return tmp; }2.利用union联合体共用内存空间特性，使用char数组来改变/** * 代码中的类名、方法名、参数名已经指定，请勿修改，直接返回方法规定的值即可 * * * @param n int整型 * @return int整型 */ typedef union { int i; unsigned char c[4] } inc_u; int convert(int n ) { // write code here inc_u x; //这里也可以用新开辟空间进行置换 x.i = n; //利用按位异或运算可叠加、可还原性 x.c[0] ^= x.c[3], x.c[3] ^= x.c[0], x.c[0] ^= x.c[3]; //首尾两字节对调 x.c[1] ^= x.c[2], x.c[2] ^= x.c[1], x.c[1] ^= x.c[2]; //中间两字节对调 return x.i; /* 按位<<到正确位置，并用|拼装 return (x.c[0]<<24)|(x.c[1]<<16)|(x.c[2]<<8)|x.c[3]; */ }3. 使用按位与运算保留以获取每个字节，然后按位左移到正确位置并拼接/** * 代码中的类名、方法名、参数名已经指定，请勿修改，直接返回方法规定的值即可 * * * @param n int整型 * @return int整型 */ int convert(int n ) { // write code here return (((n & 0xff000000)>>24) | ((n & 0x00ff0000)>>8 ) | ((n & 0x0000ff00)<<8 ) | ((n & 0x000000ff)<<24); //按位与时，遇0清零，遇1保留 ); }4. 使用预定义好的宏函数本条方法参考 https://www.codeproject.com/Articles/4804/Basic-concepts-on-Endianness文中提到网络上常用的套接字接口（socket API）指定了一种称为网络字节顺序的标准字节顺序，这个顺序其实就是大端模式；而当时，同时代的 x86 系列主机反而是小端模式。所以就促使产生了如：{message type="success" content="ntohs() convert Network order TO Host order in Short (16 bit 大转小);ntohl() convert Network order TO Host order in Long (32 bit 大转小);htons() convert Host order TO Network order in Short (16 bit 小转大);htonl() convert Host order TO Network order in Long (32 bit 小转大)."/}所以我们这里使用 32 bit 小转大的 htonl() 宏函数来解决这个问题。/** * 代码中的类名、方法名、参数名已经指定，请勿修改，直接返回方法规定的值即可 * * * @param n int整型 * @return int整型 */ int convert(int n ) { // write code here return htonl(n); ); }
一、什么是$clog2clog2 这是一个系统函数，第一次出现于Verilog-2005版本中，在IEEE中处在17.11.1节的math functions中，因为log2是2进制的对数，所以这个系统函数在电路设计的计算位宽时体现出了自身的方便性，需要注意的是，这里的$clog2是向上取整的一个系统函数,比如{message type="success" content="$clog2(5) 虽然真实的值为2.3，但经过向上取整后，最后的输出为3"/}二、$clog2的优势和案例在老的IEEE verilog版本中，假如不用clog2去计算位宽，我们可能需要如下的function函数来进行位宽计算，这个函数本身很好理解，即通过移位去检测depth的位宽，之后我们需要再将计算得到的数字使用在端口定义的过程中。function integer clog2( input integer depth ); begin if(depth == 0) clog2 = 1; else if(depth != 0) for(clog2 = 0; depth > 0;clog2 = clog2 + 1) depth = depth >> 1; end endfunction但是引入$clog2后，原function可以简化为如下的过程,很显然，通过对系统函数 $clog2的使用，我们大大减少了设计时端口宽度定义时需要code的量。module clog2(a,b); parameter depth = 2034; input [$clog2(depth)-1:0] a; output [$clog2(depth)-1:0]b; //details about the design endmodule 三、额外补充在Xlinix的官网的“44586 - 13.2 Verilog $clog2 function implemented improperly”中，作者发现了13.2版本的Xlinix的ISE对clog2系统函数的错误计算，按照文章中所言：“The $clog2 function returns the ceiling of the logarithm to the base e (natural logarithm) rather than the ceiling of the logarithm to the base 2.”意味着13.2版本的ISE以e为底计算clog2，而非以2为底，官方的回复是ISE 13.2 仅支持Verilog-2001，这个问题在ISE 14.1中进行了修复，所以读者假如使用的开发套件是老版本的，或者不支持Verilog-2005，都有可能因为使用clog2产生问题，需注意。具体额外补充参考如下。44586 - 13.2 Verilog $clog2 function implemented improperly
前期工作1、.fsdb文件在使用Makefile文件前，先在测试文件中加入这样一句。initial begin $fsdbDumpfile("tb.fsdb");//这个是产生名为tb.fsdb的文件 $fsdbDumpvars; end需要注意：对于用于仿真的testbench，需要额外建立一个 initial 块，调用产生有关 fsdb 格式的波形文件：首先调用 fsdbDumpfile 函数，产生一个叫 .fsdb 的波形文件然后调用 fsdbDumpvars 函数，声明需要保存那些信号的波形，括号内不加任何参数，则默认全部保存。2、 filelist.f文件filelist.f里存放所有需要仿真的.v文件。创建filelist.f的方法：find -name "*.v" >filelist.f1. Makefile作用？编写makefile文件本质上是帮组make如何一键编译，进行批处理，makefile文件包含的规则命令使我们不需要繁琐的操作，提高了开发效率。Makefile可以根据指定的依赖规则和文件是否有修改来执行命令。常用来编译软件源代码，只需要重新编译修改过的文件，使得编译速度大大加快。2. Makefile应用利用Makefile 实现简单的前端设计流程，包括VCS编译，Verdi仿真，DC综合，后续流程待补充。目录结构#use "make" for help help: @echo "make help" @echo "make com to compile" @echo "make sim to run simulation" @echo "make clean to delete temporary files" #need to midify design name design_name = div_top fsdb_name = $(design_name).fsdb # use command "make com" to run vsc and product fsdb file com: cd RTL && vcs \ -full64 \ -f flist.f \ -debug_all \ -l com.log \ +v2k \ -P ${Verdi_HOME}/share/PLI/VCS/LINUXAMD64/novas.tab ${Verdi_HOME}/share/PLI/VCS/LINUXAMD64/pli.a # cd RTL && ./simv -l sim.log +fsdbfile+$(fsdb_name) #simulation:product fsdb file and sim log sim: ./RTL/simv cd RTL && ./simv -l sim.log +fsdbfile+$(fsdb_name) # use verdi to observe the waveform verdi: cd RTL && verdi \ +v2k \ -f flist.f \ -ssf $(fsdb_name) & #use fsdb file # run dc for synthesize syn: cd dc_script && dc_shell -64bit -topographical -f top_syn.tcl | tee -i syn.log #delete all files except .v and makefile clean: #rm -rf `ls | grep -v "Makefile"|grep -v "flist.f" | grep -v "\.v" | grep -v "dc_script"` make -C RTL clean make -C dc_script clean/RTL目录下MakeFile#delete temporary files clean: rm -rf `ls | grep -v "Makefile"|grep -v "flist.f" | grep -v "\.v"` dc_script目录下Makefile#delete temporary files clean: rm -rf `ls | grep -v "Makefile"|grep -v "script" | grep -v ".*.tcl"` make com ：调用vcs编译make sim：调用vcs仿真make verdi 波形,shifrt+l可刷新重新编译结果make clean 删除所有子目录下的临时生成文件 详细命令 执行“make vcs” 编译仿真执行“make verdi” 打开波形verdi常用快捷键ctrl+w: 添加信号到波形图h： 在波形窗口显示详细的信号名（路径）File>save signal，命名*.rc,下次直接打开rc文件就行c/t: 修改信号的颜色（t可以直接切换颜色）在波形窗口显示状态机的名字：在rtl窗口，tools>Extract internative FSM ,可选first stage(仅展开目前所指定的FSM state)，all stage (展开所有的FSM state)改变颜色填充波形：Tools>waveform>view options>waveformpane> paint waveform with specified color/pattern在rtl窗口按x: 标注出信号的值z: 缩小波形窗口Z： 放大波形窗口f: 全屏l: 上一个视图L: 重新加载设计波形或文件n: 向前查找N： 向后查找ctrl+→: 向右移动半屏ctrl+←: 向左移动半屏双击信号波形： 跳转到rtl中信号位置，并高亮新号b: 跳到波形图开头e: 跳到波形图尾部2.不使用Makefile直接执行vcs -R -f flist.f -full64 -fsdb -l name.logverdi -f flist.f -ssf name.fsdb
前言（TCL综述）TCL(Tool Command Language)是一种解释执行的脚本语言(Scripting Language)。 它提供了 通用的编程能力：支持变量、过程和控制结构；同时 TCL还拥有一个功能强大的固有的核心命令集。由于TCL的解释器是用一个C\C++语言的过程库实现的，因此在某种意义上我们又可以把TCL看作一个C库，这个库中有丰富的用于扩展TCL命令的C\C++过程和函数，可以很容易就在C\C++应用程序中嵌入TCL，而且每个应用程序都可以根据自己的需要对TCL语言进行扩展。我们可以针对某一特定应用领域对TCL语言的核心命令集进行扩展，加入适合于自己的应用领域的扩展命令，如果需要，甚至可以加入新的控制结构，TCL解释器将把扩展命令和扩展控制结构与固有命令和固有控制结构同等看待。扩展后的TCL语言将可以继承TCL 核心部分的所有功能，包括核心命令、控制结构、数据类型、对过程的支持等。根据需要，我们甚至可以屏蔽掉TCL的某些固有命令和固有控制结构。通过对TCL的扩展、继承或屏蔽，用户用不着象平时定义一种计算机语言那样对词法、语法、语义、语用等各方面加以定义，就可以方便的为自己的应用领域提供一种功能完备的脚本语言。TCL良好的可扩展性使得它能很好地适应产品测试的需要，测试任务常常会由于设计和需求的改变而迅速改变，往往让测试人员疲于应付。利用TCL的可扩展性，测试人员就可以迅速继承多种新技术，并针对产品新特点迅速推出扩展TCL命令集，以用于产品的测试中，可以较容易跟上设计需求的变化。另外，因为TCL是一种比C\C++ 语言有着更高抽象层次的语言，使用TCL可以在一种更高的层次上编写程序，它屏蔽掉了编写C\C++程序时必须涉及到的一些较为烦琐的细节，可以大大地提高开发测试例的速度。而且， 使用TCL语言写的测试例脚本，即使作了修改，也用不着重新编译就可以调用TCL解释器直接执行。可以省却不少时间。TCL 目前已成为自动测试中事实上的标准。{mtitle title="目录"/}前言（TCL综述）语法脚本，命令和单词符号置换(substitution)变量置换variable subtitution命令置换command substitution反斜杠置换backslash substitution双引号和花括号变量简单变量数组append和increxprListconcatlindexllengthlinsertlreplacelrangelappendlsearch控制流if循环命令：while 、for 、 foreachwhileforbreak和continue命令switchsource过程（procedure）附录（Tcl的安装）直接打开终端(terminal)，输入 sudo apt install tcl即可进行安装，这里的截图是笔者安装成功后的实例。之后输入tclsh即可语法脚本，命令和单词符号一个TCL脚本可以包含一个或多个命令。命令之间必须用换行符或分号隔开，下面的两个脚本都是合法的：set a 1 set b 2 或使用分号隔开 set a 1；set b 2TCL解释器对一个命令的求值过程分为两部分：分析和执行。在分析阶段，TCL 解释器运用规则把命令分成一个个独立的单词，同时进行必要的置换(substitution)； 在执行阶段，TCL 解释器会把第一个单词当作命令名，并查看这个命令是否有定义，如果有定义就激活这个命令对应的C/C++过程，并把所有的单词作为参数传递给该命令过程，让命令过程进行处理。置换(substitution)TCL解释器在分析命令时，把所有的命令参数都当作字符串看待，例如：%set x 10 //定义变量x,并把x的值赋为10 10 %set y x+100 //y的值是x+100，而不是我们期望的110 x+100上例的第二个命令中，x被看作字符串x+100的一部分，如果我们想使用x的值’10’ ，就必须告诉TCL解释器：我们在这里期望的是变量x的值，而非字符’x’。怎么告诉TCL解释器呢，这就要用到TCL语言中提供的置换功能。置换功能分为三种.TCL提供三种形式的置换：变量置换、命令置换和反斜杠置换。每种置换都会导致一个或多个单词本身被其他的值所代替。置换可以发生在包括命令名在内的每一个单词中，而且置换可以嵌套。变量置换variable subtitution变量置换由一个＄符号标记，变量置换会导致变量的值插入一个单词中。例如之前的一个例子%set x 10 //定义变量x,并把x的值赋为10 10 %set y x+100 //y的值是x+100，而不是我们期望的110 x+100 %set y $x+100 //y的值是我们期望的110 110命令置换command substitution命令置换是由[]括起来的TCL命令及其参数，命令置换会导致某一个命令的所有或部分单词被另一个命令的结果所代替。例如：%set y [expr $x+100] 110这里当TCL解释器遇到字符’[‘时，它就会把随后的expr作为一个命令名，从而激活与expr对应的C/C++过程，并把expr和变量置换后得到的10+100传递给该命令过程进行处理。反斜杠置换backslash substitutionTCL语言中的反斜杠置换类似于C语言中反斜杠的用法，主要用于在单词符号中插入诸如换行符、空格、[、$等被TCL解释器当作特殊符号对待的字符。%set msg money\ \$3333\ \nArray\ a\[2] //这个命令的执行结果为： money $3333 Array a[2]双引号和花括号除了使用反斜杠外，TCL提供另外两种方法来使得解释器把分隔符和置换符等特殊字符当作普通字符，而不作特殊处理，这就要使用双引号和花括号({})。TCL解释器对双引号中的各种分隔符将不作处理，但是对换行符 及＄和[]两种置换符会照常处理。而在花括号中，所有特殊字符都将成为普通字符，失去其特殊意义，TCL解释器不会对其作特殊处理。%set y "$x ddd" 100 ddd %set y {/n$x [expr 10+100]} /n$x [expr 10+100]注释TCL中的注释符是＃，＃和直到所在行结尾的所有字符都被TCL看作注释，TCL解释器对注释将不作任何处理。不过，要注意的是，＃必须出现在TCL解释器期望命令的第一个字符出现的地方，才被当作注释。%set a 100 # Not a comment wrong # args: should be "set varName ?newValue?" %set b 101 ; # this is a comment 101变量变量分为简单变量和数组简单变量一个 TCL 的简单变量包含两个部分：名字和值。名字和值都可以是任意字符串。% set a 2 2 set a.1 4 4 % set b $a.1 2.1在最后一个命令行，我们希望把变量a.1的值付给b，但是TCL解释器在分析时只把$符号之后直到第一个不是字母、数字或下划线的字符(这里是’.’)之间的单词符号(这里是’a’)当作要被置换的变量的名字，所以TCL解释器把a置换成2,然后把字符串“2.1”付给变量b。这显然与我们的初衷不同。当然，如果变量名中有不是字母、数字或下划线的字符，又要用置换，可以用花括号把变量名括起来。例如：%set b ${a.1} 4数组数组是一些元素的集合。TCL的数组和普通计算机语言中的数组有很大的区别。在TCL中，不能单独声明一个数组，数组只能和数组元素一起声明。数组中，数组元素的名字包含两部分：数组名和数组中元素的名字，TCL中数组元素的名字（下标〕可以为任何字符串。 例如：set day(monday) 1 set day(tuesday) 2 set a monday set day(monday) 1 set b $day(monday) //b 的值为 1 ，即 day(monday) 的值。 set c $day($a) //c 的值为 1 ，即 day(monday) 的值。其他命令unset% unset a b day(monday)上面的语句中删除了变量a、b和数组元素day(monday)，但是数组day并没有删除，其他元素还存在，要删除整个数组，只需给出数组的名字。append和incr这两个命令提供了改变变量的值的简单手段。append命令把文本加到一个变量的后面，例如：% set txt hello hello % append txt "! How are you" hello! How are youincr命令把一个变量值加上一个整数。incr要求变量原来的值和新加的值都必须是整数。expr可以进行基本的数学函数计算%expr 1 ＋ 2*3 7Listlist这个概念在TCL中是用来表示集合的。TCL中list是由一堆元素组成的有序集合，list可以嵌套定义，list每个元素可以是任意字符串，也可以是list。下面都是TCL中的合法的list：{} //空list {a b c d} {a {b c} d} //list可以嵌套list是TCL中比较重要的一种数据结构，对于编写复杂的脚本有很大的帮助list语法： list ? value value…?这个命令生成一个list，list的元素就是所有的value。例： % list 1 2 {3 4} 1 2 {3 4}使用置换将其相结合% set a {1 2 3 4 {1 2}} 1 2 3 4 {1 2} % puts $a 1 2 3 4 {1 2}concat语法:concat list ?list…?这个命令把多个list合成一个list，每个list变成新list的一个元素。% set a {1 2 3} 1 2 3 % set b {4 5 6} 4 5 6 % concat $a $b 1 2 3 4 5 6lindex语法：lindex list index返回list的第index个(0-based)元素。例：% lindex {1 2 {3 4}} 2 3 4llength语法：llength list返回list的元素个数。例% llength {1 2 {3 4}} 3 % set a {1 2 3} 1 2 3 % llength $a 3linsert语法：linsert list index value ?value…?返回一个新串，新串是把所有的value参数值插入list的第index个(0-based)元素之前得到。例:% linsert {1 2 {3 4}} 1 7 8 {9 10} 1 7 8 {9 10} 2 {3 4} % linsert {1 2 {3 4}} 1 {1 2 3 {4 5}} 1 {1 2 3 {4 5}} 2 {3 4} % set a {1 2 3} 1 2 3 % linsert $a 1 {2 3 4} 1 {2 3 4} 2 3lreplace语法：lreplace list first last ?value value …?返回一个新串，新串是把list的第firs (0-based)t到第last 个(0-based)元素用所有的value参数替换得到的。如果没有value参数，就表示删除第first到第last个元素。例：% lreplace {1 7 8 {9 10} 2 {3 4}} 3 3 1 7 8 2 {3 4} % lreplace {1 7 8 2 {3 4}} 4 4 4 5 6 1 7 8 2 4 5 6 % set a {1 2 3} 1 2 3 % lreplace $a 1 2 4 5 6 7 1 4 5 6 7 % lreplace $a 1 end 1lrange语法:lrange list first last返回list的第first (0-based)到第last (0-based)元素组成的串,如果last的值是end。就是从第first个直到串的最后。例：% lrange {1 7 8 2 4 5 6} 3 end 2 4 5 6 % set a {1 2 3} 1 2 3 % lrange $a 0 end 1 2 3lappend语法：lappend varname value ?value…?把每个value的值作为一个元素附加到变量varname后面，并返回变量的新值，如果varname不存在，就生成这个变量。例：% set a {1 2 3} 1 2 3 % lappend a 4 5 6 1 2 3 4 5 6lsearch语法：lsearch ?-exact? ?-glob? ?-regexp? list pattern返回list中第一个匹配模式pattern的元素的索引，如果找不到匹配就返回-1。-exact、-glob、 -regexp是三种模式匹配的技术。-exact表示精确匹配；-glob的匹配方式和string match命令的匹配方式相同；-regexp表示正规表达式匹配。缺省时使用-glob匹配。例:% set a { how are you } how are you % lsearch $a y* 2 % lsearch $a y? -1-all 返回一个列表，返回的列表中的数值就是字符在列表中的位置默认全局匹配，返回第一个字符在列表中的位置，其位缺省状态% lsearch {a b c d e} c 2 % lsearch -all {a b c a b c} c 2 5 % lsearch {a b c d c} c 2匹配不到返回-1% lsearch {a b c d e} g -1控制流主要是对于所有的控制流，包括 if、while、for、foreach、switch、break、continue 等以及过程，if语法： if test1 body1 ?elseif test2 body2 elseif…. ? ?else bodyn?TCL先把test1当作一个表达式求值，如果值非0，则把body1当作一个脚本执行并返回所得值，否则把test2当作一个表达式求值，如果值非0，则把body2当作一个脚本执行并返回所得值……。例如：if { $x>0 } { ..... }elseif{ $x==1 } { ..... }elseif { $x==2 } { .... }else{ ..... }if { $x<0 } { puts "x is smaller than zero" } elseif {$x==1} { puts "x is equal 1" } elseif {$x==2} { puts "x is equal 2" } else { puts "x is other" } 这里需要注意的是，if 和{之间应该有一个空格，否则TCL解释器会把’if{‘作为一个整体当作一个命令名，从而导致错误。‘{‘一定要写在上一行，因为如果不这样，TCL 解释器会认为if命令在换行符处已结束，下一行会被当成新的命令，从而导致错误的结果需要将}{ 分开写，否则会报错extra characters after close-brace循环命令：while 、for 、 foreachwhile语法为: while test body参数test是一个表达式，body是一个脚本，如果表达式的值非0，就运行脚本，直到表达式为0才停止循环，此时while命令中断并返回一个空字符串。例如：假设变量 a 是一个链表，下面的脚本把a 的值复制到b：% #首先生成一个集合 % set a {1 2 3 4} 1 2 3 4 % set b " " % #计算生成集合的长度（从0开始这里需要减去1例如：0-3一共有四个数） % set i [expr [llength $a] -1] 3 #接下来进行判断，将集合a中的元素全部按顺序写入b中 % while {$i>=0} { #思考执行该行代码替换会有怎样的结果打印出来 #lappend b [lindex $a $i] lappend b [lindex $a [expr [llength $a] - 1 - $i]] incr i -1 } #打印观察结果 % puts $b 1 2 3 4对代码进行分析set 变量a为一个list，b为一个空list然后计算列表里有几个元素，将其减一后的值赋值给i，这里减一的目的是从零开始计数会多一个开始进行循环，首先i的值是4大于0，表达式为真，开始执行脚本。脚本为将数组a的第i个位置的元素添加到b list 里，然后给i减一同时进行下一次判断即可。最后输出b的值for语法为: for init test reinit body参数init是一个初始化脚本，第二个参数test是一个表达式，用来决定循环什么时候中断，第三个参数reinit是一个重新初始化的脚本，第四个参数body也是脚本，代表循环体。下例与上例作用相同：（注意这里复制打印顺序的不同）% set a {1 2 3 4} 1 2 3 4 % set b " " % for {set i [expr [llength $a] -1]} {$i>=0} {incr i -1} { lappend b [lindex $a $i] } % puts $b 4 3 2 1例% for {set i 0} {$i<4} {incr i} { puts "I is: $i " } I is: 0 I is: 1 I is: 2 I is: 3 foreach这个命令有两种语法形式1, foreach varName list body第一个参数varName是一个变量，第二个参数list 是一个表(有序集合)，第三个参数body是循环体。每次取得链表的一个元素，都会执行循环体一次。 下例与上例作用相同：% set a {1 2 3 4} 1 2 3 4 % set b " " % foreach i $a { set b [linsert $b 0 $i] } % puts $b 4 3 2 1% foreach var {a b c d e f} { puts $var } a b c d e f2, foreach varlist1 list1 ?varlist2 list2 ...? Body这种形式包含了第一种形式。第一个参数varlist1是一个循环变量列表，第二个参数是一个列表list1，varlist1中的变量会分别取list1中的值。body参数是循环体。 ?varlist2 list2 …?表示可以有多个变量列表和列表对出现。例如：set x {} foreach {i j} {a b c d e f} { lappend x $j $i }这时总共有三次循环，x的值为”b a d c f e”。% foreach i {a b c} j {d e f g} { puts $i puts $j } a d b e c f gset x {} foreach i {a b c} j {d e f g} { lappend x $i $j }这时总共有四次循环， x的值为”a d b e c f {} gset x {} foreach i {a b c} {j k} {d e f g} { lappend x $i $j $k }这时总共有三次循环，x的值为”a d e b f g c {} {}”。例子：break和continue命令在循环体中，可以用break和continue命令中断循环。其中break命令结束整个循环过程，并从循环中跳出，continue只是结束本次循环这里有一个特别好的例子说明：这里首先进行给一个list，然后使用foreach循环进行写入数据当遇见break时候直接退出了循环，而continue仅仅只是跳出此次循环继续向b里写入数% set b {} % set a {1 2 3 4 5} 1 2 3 4 5 % foreach i $a { if {$i == 4} break set b [linsert $b 0 $i] } % puts $b 3 2 1% set b {} % set a {1 2 3 4 5} 1 2 3 4 5 % foreach i $a { if {$i == 4} continue set b [linsert $b 0 $i] } % puts $b 5 3 2 1switch和 C 语言中 switch 语句一样，TCL 中的 switch 命令也可以由 if 命令实现。只是书写起来较为烦琐。 switch 命令的语法为: switch ? options? string { pattern body ? pattern body …?}注意这里进行的是字符匹配set x a; set t1 0;set t2 0;set t3 0; switch $x { a - b {incr t1} c {incr t2} default {incr t3} } puts "t1=$t1,t2=$t2，t3=$t3"x=a时执行的是t1加2其中 a 的后面跟一个’－’表示使用和下一个模式相同的脚本。default 表示匹配任意值。一旦switch 命令 找到一个模式匹配，就执行相应的脚本，并返回脚本的值，作为 switch 命令的返回值。sourcesource 命令读一个文件并把这个文件的内容作为一个脚本进行求值以上边的switch第一段代码为例使用VIM新建一个文件，写入文件后保存退出vim switch1.tcl键入wish然后输入source switch1.tcl过程（procedure）TCL 支持过程的定义和调用，在 TCL 中,过程可以看作是用 TCL 脚本实现的命令，效果与 TCL的固有命令相似。我们可以在任何时候使用 proc 命令定义自己的过程，TCL 中的过程类似于 C中的函数。TCL 中过程是由 proc 命令产生的：例如：% proc add {x y } {expr $x+$y} roc 命令的第一个参数是你要定义的过程的名字，第二个参数是过程的参数列表，参数之间用空格隔开，第三个参数是一个 TCL 脚本，代表过程体。 proc 生成一个新的命令，可以象固有命令一样调用：% add 1 2 3