首页
归档
友情链接
关于
Search
1
在wsl2中安装archlinux
80 阅读
2
nvim番外之将配置的插件管理器更新为lazy
58 阅读
3
2018总结与2019规划
54 阅读
4
PDF标准详解(五)——图形状态
33 阅读
5
为 MariaDB 配置远程访问权限
30 阅读
心灵鸡汤
软件与环境配置
博客搭建
从0开始配置vim
Vim 从嫌弃到依赖
archlinux
Emacs
MySQL
Git与Github
AndroidStudio
cmake
读书笔记
菜谱
编程
PDF 标准
从0自制解释器
qt
C/C++语言
Windows 编程
Python
Java
算法与数据结构
PE结构
登录
Search
标签搜索
c++
c
学习笔记
windows
文本操作术
编辑器
NeoVim
Vim
win32
VimScript
Java
emacs
linux
文本编辑器
elisp
反汇编
OLEDB
数据库编程
数据结构
内核编程
Masimaro
累计撰写
308
篇文章
累计收到
27
条评论
首页
栏目
心灵鸡汤
软件与环境配置
博客搭建
从0开始配置vim
Vim 从嫌弃到依赖
archlinux
Emacs
MySQL
Git与Github
AndroidStudio
cmake
读书笔记
菜谱
编程
PDF 标准
从0自制解释器
qt
C/C++语言
Windows 编程
Python
Java
算法与数据结构
PE结构
页面
归档
友情链接
关于
搜索到
147
篇与
的结果
2021-04-11
顺序容器
所有容器类都共享公共的接口,不同容器按不同的方式进行扩展,这个公共接口使得学习容器更加容器。我们基于这种容器所学习的内容也都适用于其他容器。每种容器都提供了不同的性能和功能权衡一个容器就是一些特定类型对象的集合。顺序容器为程序员提供了控制元素存储顺序的能力。这种顺序不依赖于元素的值,而是与元素加入容器的位置相对应。顺序容器概述所谓的顺序容器是指,在内存中数据存储有一定顺序。数据结构中的顺序容器有:可变数组、队列、数组、链表、栈。c++ 标准库中的顺序容器提供了快速顺序访问元素的能力。但是这些容器在一下方面都有不同的性能折中向容器中添加或者删除元素的代价非顺序访问容器中元素的代价标准库中顺序容器主要有:vector:可变大小的数组。支持快速随机访问,在尾部之外插入或者删除元素可能会很慢dque:双端队列,支持快速随机访问,在头尾位置插入/删除元素速度很快list:双向连标,只支持双向顺序访问,在list中任何位置进行插入删除操作速度都很快forward_list: 单向链表,只支持单向顺序访问,在链表任何位置插入删除元素速度很快array: 固定大小的数组,支持快速随机访问,不能添加或者删除元素string: 与vector容器类似,但专门用于保存字符,随机访问快。在尾部插入与删除速度快c++ 标准库中的容器是经过精心优化设计过的。性能通常会是同类数据结构中最好的。现代c++ 程序应该使用标准库容器,而不是原始的数据结构(如内置数组)通常使用vector 是最好的选择,除非你有很好的理由选择其他容器一下是一些选择容器的基本原则:除非你有很好的理由选择其他容器,否则使用vector如果你的程序有很多小的元素,且额外开销很严重,否则不要使用list或者forward_list如果程序要求随机访问元素,应该使用vector 或者deque如果程序要求在容器中间插入或者删除元素,应该使用list或者forward_list如果程序要在头尾位置插入或者删除元素,但是不会在中间位置插入删除元素,则应该使用deque如果程序只有在读取输入时才需要在容器中间插入元素,随后需要随机访问元素,则: 6.1 首先确定是否真的需要在容器中间位置添加元素。当处理输入数据时通常很容易向vector中添加数据,然后再调用标准库的sort函数,来重排元素,避免在中间位置添加元素 6.2 如果必须在中间位置插入元素考虑在输入阶段使用list,一旦输入完成,将list中的内容拷贝到一个vector中如果你不确定该使用哪种容器,可以在程序中只使用vector 和list公共的操作,不使用下标操作,使用迭代器,避免随机访问容器库概述迭代器迭代器是访问容器中元素的公共接口所有迭代器都通过解引用运算符来实现这个操作。标准库中的所有迭代器都定义了递增运算符,从当前元素移动到下一个元素。部分容器的迭代器也定义了递减运算符,用于从一个元素移动到上一个元素一个迭代器范围是由一对迭代器来表示的。两个迭代器分别指向同一个容器的元素或者尾元素之后的位置。如果两个迭代器满足以下两个条件,则这两个迭代器构成一个迭代器范围:它们指向同一个容器中的元素,或者是容器最后一个元素之后的位置我们可以通过反复递增begin 来到达end位置,换句话说,end不在begin之前如果两个迭代器构成一个迭代器范围,则:如果begin和end相等,则范围为空如果begin和end不等,则范围至少包含一个元素,且begin指向该范围中的第一个元素我们可以对begin递增若干次,使得begin== end容器类型成员每个容器都定义了多个类型,我们已经使用了其中的3种:size_type、iterator、const_iterator除了正向的迭代器,容器库还提供了反向遍历容器的迭代器,反向迭代器就是一种反向遍历容器的迭代器。与正向迭代器相比各种操作的含义也都发生了颠倒。例如对一个反向迭代器执行++操作,会得到上一个元素。剩下的就是类型别名了。通过类型别名,我们可以在不了解容器中元素类型的情况下使用它,如果需要元素类型可以使用容器的value_type ,如果需要元素类型的一个引用可以使用reference或者const_reference。begin 和 end 成员begin 和 end 操作生成一个指向容器中第一个元素和尾元素之后位置的迭代器范围。begin和end有多个版本。带r的版本返回反向迭代器,带c的版本返回const型迭代器容器定义和初始化可以将一个容器初始化为另一个容器的拷贝将一个新容器创建为另一个容器的拷贝的方法有两种:可以直接拷贝整个容器,或者拷贝由一个迭代器对指定的元素范围为了创建一个容器为另一个容器的拷贝,两个容器的类型及其元素类型必须匹配。当传递迭代器参数来拷贝一个范围时,就不要求容器类型是相同的了。而且新容器和原容器中的元素类型也可以不同,只要能将拷贝的元素转化为要初始化的容器的元素类型即可在新标准中我们对一个容器进行列表初始化标准库array在定义之处就应该给出具体的大小,而且后续不允许修改它的大小我们不能直接对内置数组执行拷贝或者对象赋值操作,但是array对象允许赋值和swap我们直接使用 = 运算符来将一个容器赋值为另一个容器的拷贝,但是要求容器类型完全相同,array也支持这种操作顺序容器(array除外)还定义了一个assign的成员,assign操作用参数所指定的元素替换左边容器中的所有元素list<string> name; vector<const char*> oldstyle; name = oldstyle; //错误 = 要求两侧容器类型完全相同 name.assign(oldstyle);可以使用swap交换两个容器中的内容,要求两个容器类型完全相同。除了array外,swap不对任何元素进行拷贝、删除或者插入操作,可以保证在常量时间内完成容器大小操作每个容器都有三大与大小相关的操作。size: 返回容器中元素数目empty: 当容器中元素数量为0时,返回true,否则返回falsemax_size: 返回一个大于或者等于该类型容器所能容纳的最大元素数的值关系运算符除了无序容器外的所有容器都支持关系运算符关系运算符左右两边的运算对象必须是相同类型的容器,且必须保存相同类型的元素比较的过程与比较string大小的过程类似如果两个容器具有相同大小且所有元素都两辆对应相等,则两个容器相等。否则两个容器不等如果两个容器大小不同,但较小容器中每个元素都等于较大容器中对应元素。则较小容器大于较大容器如果两个容器都不是另一个容器的前缀自序列,则它们的比较结果取决于第一个不相等的元素的比较结果容器的相等运算符实际上是使用元素的==运算符实现比较的。而其他关系是使用元素的< 运算符顺序容器的操作向顺序容器中添加元素push_back:将内容追加到容器尾部push_front: 将内容添加到容器的首部insert: 在容器的特定位置插入0个或者多个元素,返回插入元素位置的迭代器emplace_back、emplace_front、emplace: 这些函数是直接在容器内部进行元素构造,而上述函数是将内容进行拷贝。从效率上讲emplace 函数会高一些使用这些操作时必须记得不同类型的容器使用不同的元素分配策略,而这些策略直接影响性能。访问顺序容器每个顺序容器中都有一个front 函数,返回容器内第一个元素的引用。而除了forward_list 之外的所有顺序容器都有一个back成员函数。另外可以使用at来访问随机位置的元素记住,这些访问函数返回的都是引用删除元素pop_front: 删除首元素pop_back: 删除尾元素erase: 可以从容器中删除指定位置的元素,可以传入一个范围,删除指定范围内的元素特殊的 forward_list 操作在对forward_list 进行增删操作的时候,需要找到对应位置的前驱节点,而单向链表无法很容器的找到一个节点的前驱节点。因此对单向链表,提供了类似 insert_after、emplace_after、erase_after等操作改变容器大小可以使用resize 来增大或者缩小容器大小,如果是缩小容器大小,则指向被删除元素的迭代器、引用、指针都会失效容器操作可能使迭代器失效在向容器中添加元素后:如果容器是vector或者string,且存储空间被重新分配,则指向容器的迭代器、指针都会失效。如果存储空间未重新分配,指向插入位置之前的迭代器、指针、引用仍然有效,但是指向插入位置之后元素的迭代器、指针和引用将会失效对于deque,插入到首尾位置之外的任何位置都会导致迭代器、指针和引用失效。如果在首尾位置添加元素,迭代器会失效,但指向存在的元素的引用和指针不会失效对于list和forward_list,指向容器的迭代器、指针和引用仍然有效删除一个元素后,指向原来被删除元素的迭代器、指针和引用都会失效。对于forward_list 和list来说,指向容器其他位置的迭代器、引用和指针仍然有效对于deque,如果在首尾之外的任何位置删除元素,那么指向被删除元素以外的其他元素的迭代器、引用和指针也会失效。如果是删除deque的尾元素,则尾后迭代器也会失效。但是其他迭代器、引用和指针不受影响,如果删除首元素,这些也不会受到影响对于vector和string,指向被删除元素之前元素的迭代器、引用和指针仍然有效删除元素时尾后迭代器总是会失效使用insert插入元素后可以保存返回的迭代器,然后用该迭代器进行迭代可以保证迭代器有效不要保存end返回的迭代器vector 容器是如何增长的为了支持快速随机访问,vector 将元素连续存储。如果往容器中添加一个新元素时,发现容器空间已经不够了,就需要重新分配空间。并将已有元素逐一拷贝到新的内存空间中,然后添加新元素。为了避免这种代价,标准库实现者采用了可以减少容器空间重新分配次数的策略。当不得不获取新的内存空间时,vector和string的实现通常会分配比新的空间需求更大的内存空间vector和string也提供了一些成员函数,允许我们与它的实现中内存分配部分互动。capacity: 告诉我们容器在不扩张内存空间的情况下可以容纳多少个元素reserve: 允许我们同志容器它应该准备保存多少个元素一般来讲,vector 的实现采用的策略似乎是在每次需要分配新内容空间时将当前容量翻倍额外的string操作除了顺序容器共同的操作之外,string类还提供了一些额外的操作。这些操作中的大部分要么是提供string类和C风格字符串之间的互相转换,要么是增加了允许我们用下标代替迭代器的版本。substr: 返回一个string,它是原始string的一部分或者全部的拷贝可以使用insert、erase、assign 来改变字符串的内容append可以在字符串尾部添加一个新字符串; replace 进行查找替换搜索操作标准库中提供了6个不同的搜索函数,每个函数有4中不同形式的重载版本。每个搜索操作都返回string::size_type 值,表示匹配发生位置的下标。如果搜索失败返回一个名为string::npos 的static成员s.find(arg): 查找字符串中第一次出现某个字符串的位置s.rfind(arg): 查找字符串中最后一次出现某个字符串的位置s.find_first_of(arg): 在s中查找arg中任意一个字符第一次出现的位置s.find_last_of(arg): 在s中查找arg中任意一个字符最后一次出现的位置s.find_first_not_of(arg): 在s中查找第一个不在arg 中的字符s.find_last_not_of(arg): 在s中查找最后一个不在arg 中的字符compare 函数compare函数用于比较两个大小字符串,与C标准库中的strcmp类似数值转化to_string: 将数值数据转化为字符串stod: 将字符串转化为doublestof: 将字符串转化为floatstoi: 将字符串转化为intstol: 将字符串转化为longstoul: 将字符串转化为 unsigned longstoll: 将字符串转化为 long longstoull: 将字符串转化为 unsigned long longstold: 将字符串转化为 long double容器适配器适配器是标准库提供的一组概念,能使某种事物的行为看起来像另一种事物一样。一个容器适配器接受一种已有的容器类型,使其行为看起来像另一种事物一样标准库提供了三种适配器: stack、queue、priority_queue(优先级队列)所有的适配器都要求容器具有添加和删除元素的能力。stack 只要求类型容器具有 push_back、pop_back 操作因此可以使用除了array 和 forward_list 之外的任何容器类型来进行构造queue 要求容器类型具有 back、push_back、front、pop_back 因此它可以构造在list 或者deque之上但不能基于vector 构造; priority_queue 要求容器类型具有push_back、front、pop_back之外还要求容器具有随机访问的能力,所以它必须构造在vector之上
2021年04月11日
4 阅读
0 评论
0 点赞
2021-04-04
IO库
c++ 语言中不直接处理输入和输出,而是通过一族定义在标准库中的类型来处理IO,这些类型支持从设备读取数据、向设备写入数据的IO操作。设备可以是文件、控制台窗口等。还有一些IO运行内存IO,即可以从string中读写数据。IO库IO类最开始接触的c++ 中的io是我们从控制台接受输入的istream和输出到控制台中的ostream。除了基本的istream和ostream以外标准库中还定义了如下的基本类型iostream 用于读写流的基本类型istream、wistream: 从流读取数据ostream、wostream: 向流写入数据iostream、wiostream:从流中读写数据fstream 定义了读写命名文件的类型ifstream、wifstream: 从文件中读写数据ofstream、wofstream: 向文件中写入数据fstream、wfstream: 读写文件sstream 定义了读写内存string对象的类型istringstream、iwstringstream: 从string中读取数据ostringstream、owstringstream: 向string中写入数据stringstream、wstringstream: 读写string其中带w的都是款字节版本无法对io对象进行拷贝或者赋值、因此在函数中无法返回IO类型也无法传递IO类型,只能使用IO类型的引用读写一个IO对象会改变其状态,所以在函数中传递和返回IO的对象不能是const的一个流如果发生错误,其上后续的IO操作都会失败。只有当一个IO流处于无错误状态时,我们才能从它读取数据。因此代码通常应该在使用一个流之前检查它是否处于良好状态,确定一个流对象的状态最简单的方式是将它作为一个条件来使用。作为条件使用只能告诉我们流是否有效,而无法告诉我们具体发生了什么。IO库定义了一组与机器无关的iostate类型,这个类型中使用二进制位来表示每种状态。目前定义了4种错误类型:badbit: 流崩溃failbit: IO操作失败eofbit: 流到达了文件结束位置goodbit: 流未处于错误状态在实际使用时可以将具体值与这些预定义的值做位与运算,得到具体的原因流对象的rstate 成员返回当前流的状态,setstate用来设置流状态。clear不带参用来清理所有错误标志位。clear的带参版本接受一个iostate值,表示流的新状态io操作比较耗时,所以操作系统为了效率会提供缓冲机制。输入输出并不是立即执行的,操作系统提供了一个缓冲区,在适当的实际会使用缓冲区的数据,统一执行输入输出操作。导致刷新的原因有很多:程序正常结束,在main函数执行return时,会进行刷新操作缓冲区满时,会进行换新操作程序中使用操作符例如endl来显式的刷新缓冲区输出操作结束后,使用操作符unitbuf 设置流的内部状态来清空缓冲区,默认情况下cerr 是设置了unitbuf 的,因此cerr的内容都是实时刷新的一个输出流被关联到另一个输出流。当读写被关联到另一个流时,关联到的流的缓冲区会被刷新除了使用endl、flush、ends 都可以来刷新缓冲区。endl在刷新的同时会插入换行符,flush则不添加任何字符,ends会添加一个空字符如果想在每次输出后都刷新缓冲区,可以使用unitbuf 操作符,它告诉流,每次执行写操作之后都进行一个flush操作cout << unitbuf; cout << nounitbuf;如果程序崩溃,缓冲区是不会被刷新的标准库是将cin和cout关联到一起了,所以每次执行cin都会导致cout的缓冲区被刷新可以使用tie 方法将自身关联到另一个流上。tie 带参数的版本,需要传入一个指向ostream 的指针,将自己关联到这个ostream中tie 不带参数的版本用来查询自身关联到了哪个输出流上,返回对应输出流的指针,如果未被关联,则返回空指针每个输入流最多只能关联到一个输出流,但是多个输入流可以关联到同一个ostream文件IO当我们要读写一个文件时可以使用文件流对象ifstream in(ifile); //传入文件名,构造一个ifstream 并打开文件 ofstream out; //定义一个文件输出流,这个流不关联到任何文件当我们定义了空的文件流对象后可以使用open函数将对象和文件关联起来。可以手动调用close函数关闭文件。也可以在fstream对象被销毁时由它的构造函数自动调用close每个流都有一个关联的文件模式,用来指出该如何使用文件in: 以读的方式打开out: 以写的方式打开app: 每次写操作前均定位到文件尾部ate: 每次打开文件后立即定位到文件尾部trunc: 截断文件binary:以二进制的形式打开文件string 流当我们的某些工作是对文本进行处理,而其他一些工作是处理行内的单词时通常可以使用 istringstream 即要在一行字符串中取出单个单词时可以使用字符串流
2021年04月04日
6 阅读
0 评论
0 点赞
2021-03-21
类
最近好像很久没有更新过关于c++ primer 的读书笔记了,一来自己最近遇到了烦心事,中断了一段时间的读书。第二个是因为我有点想写点随笔之类的东西了,中间更新了两篇随笔《关于读书》、《我的五年计划》。第三个是因为关于类这部分的内容确实有点多了,要读完也需要花费一定时间。因此更新就慢了起来。我发现我已经忘记了如何给这类文章取名字了,还是看着以前的项目想起来的。既然我定下来了未来5年的发展计划,那么从现在开始就应该坚持下来了。定义抽象数据类型这里定义抽象数据类型就是定义一个类,只要学过c++的对定义一个类并并不陌生,这里就不再详细的说明该如何定义一个类了。这部分主要需要注意:1类的const成员函数:一般类的成员函数会隐式的传入当前对象的地址即 ClassExample::func(ClassExample* this);const型的成员函数传入的是一个const型的this指针,ClassExample::func(const ClassExample* this); 从这个角度上说不允许在常函数里面修改对象的值。同时由于const类型无法自动转化为非const类型,所以const型对象只能调用const成员函数。类的作用域:类本身就是一个作用域,类中的所有成员定义在类这个作用域中。编译器在编译类的时候分两步,首先编译成员的声明,然后编译成员函数,因此在成员函数中可以随意使用类的其他成员而不用关心这些成员出现的顺序。如果一个函数在概念上属于这个类,但是不定义为类的成员函数,一般将这个类定义在类声明的头文件中访问控制与封装一般来说定义类的时候应该将类中的数据成员定义为私有或者保护类型,通过成员函数来访问类的数据成员,这样有两个好处:当我们发现数据成员的值不正常的时候,由于类外部是无法访问到数据成员的,所以在调试时只用关注改变了该数据成员的函数即可使用者在使用时只需要关注类提供的功能,不需要知道它里面具体的实现。只用调用类方法就好了,不用关注该如何设置数据成员到此为止,书中提到了两种访问权限,public和private:public: 后定义的成员可以在整个程序内被访问private: 后定义的成员只能在类的成员函数中被访问每个访问说明符指定了接下来的成员的访问级别,其有效范围直到出现下一个访问说明符或者达到类的结尾为止使用class或者struct关键字定义的唯一区别是默认的访问控制符,class默认是private、而struct默认的是public友元在某些时候,可能必须要在类外部使用类的私有成员,这个时候可以将对应的函数或者类声明为类的友元函数或者友元类,友元函数或者友元类可以随意使用类的私有成员。如果类想把一个函数作为它的友元,只需要增加一条以friend 关键字开始的函数声明语句即可友元声明只能出现在类内部,但是在类内出现的具体位置不限,友元不是类的成员也不受它所在区域访问控制级别的约束。需要注意在设计时尽量考虑清楚是不是一定要用到友元,毕竟友元已经在一定程度上破坏了类的封装性类的其他特性除了一些基本的使用和访问权限控制外,书中还提到了类的其他特性:在类中,常有一些规模较小的函数合适于被声明成内联函数。定义在类内部的成员是自动inline的,当然也可以显式的声明为inline函数,这样就可以在类外部定义我们可以仅仅只声明而暂时不定义它,这种声明有时候被称作前向声明。这个类在声明之后,定义之前是一个不完全类型。不完全类型可以用于定义该类型的指针或者引用,也可以声明以该类型作为参数或者返回该类型的函数。对一个类来说,在创建它的对象之前必须被定义。因为编译器在创建对象的时候必须知道类对象占多少存储空间如果一个类指定了友元类,那么这个友元类的成员函数可以访问此类包括非公有成员在内的所有成员友元关系不具备传递性,每个类单独控制自己的友元类或者友元函数除了令整个类作为友元之外,还可以只为某个成员函数单独提供访问权限。当把成员函数声明为友元的时候,我们必须明确指出该成员属于哪个类如果一个类想把一组重载函数声明为友元,它需要对这组函数中的每一个分别声明类的作用域一个类就是一个作用域,在类的外部类成员都被隐藏起来了。在c++ 中,内层作用域中的同名成员会覆盖外层,当函数内部或者类内部定义了与全局作用域相同的变量时,要使用全局作用域中的变量可以使用::类构造函数相关在构造函数中初始化列表相当于先定义再赋值,而要做到对成员变量定义的同时初始化,可以使用初始值列表的形式在某些场合下初始值列表必不可少:初始化const成员或者引用成员构造函数初始值列表只说明用于初始化成员的值,而不限定初始化的具体执行顺序成员的初始化顺序与他们在类中定义的顺序一致。构造函数初始值列表中初始值的前后位置关系不会影响实际的初始化顺序最好令构造函数初始值的顺序与成员声明的顺序保持一致。而且如果可能的话,尽量避免使用某些成员初始化其他成员类的静态成员类的静态数据成员存在于任何对象之外,对象中不包含任何与静态数据成员有关的数据。类似的类的静态函数成员也不与任何对象绑定在一起。它们不包含this指针,静态函数成员不能被声明成const类型,也不能在静态函数成员中调用非类的静态成员不能在类的内部初始化类的静态成员,static关键字只能出现在类内部声明语句中,定义的时候不能加static关键字针对constexpr类型的static成员,可以在类内定义类内初始值由于静态数据成员不与类绑定,所以在计算类大小的时候可以不用考虑静态成员。这样就可以在类内定义该类型的静态数据成员,而非静态数据成员只能定义为该类型的指针或者引用+BEGIN_SRC c++class Menu{private:static Menu me1; Menu* me2; Menu& me3;};+END_SRC另外我们可是用静态成员做成员函数的默认实参
2021年03月21日
3 阅读
0 评论
0 点赞
2021-03-09
c++基础之函数
距离上次更新又过了一周,又该更新新的读书笔记了。本次更新的主要是c++中函数部分的内容c++ 中的函数与c语言中的函数大致用法或者语法是一样的,这里就不就这点详细展开了。需要注意的是c/c++中并没有规定函数中参数的求值顺序,所以在调用函数时需要特别注意,在传递实参的同时不要修改实参的值,也就是不要写类似func(i, ++i)这样的语句局部对象高级语言中,名字只是用来访问对象所在内存的一个工具,一个对象可以分为对象名和它实际所占的内存空间。这个对象名有它的作用域,对象所在内存有自己的声明周期。这二者不是一个概念,不要弄混淆了。变量的作用域一般只在它所定义的语句块中起作用。但是变量本身根据定义位置不同,生命周期也不同,例如int func() { for(int i = 0; i < 10; i++); }上述代码中i这个名称仅仅在for循环中有效,而i所指代的变量,它会一直持续到函数结束(可以参考鄙人曾经写过的关于c++反汇编分析相关的内容)根据定义位置的不同,变量分为局部变量和全局变量;局部变量,或者在书中有一个新的名字叫自动对象,对于局部变量来说,当代码执行到变量定义语句时创建该对象,当到达定义所在语句块的末端时销毁它,只存在于块执行期间的对象称作自动对象全局变量:定义在函数外部的变量称之为全局变量,全局变量的生命周期从程序启动时创建到程序结束时销毁。除了这两类以外,还有在函数中使用static关键字定义的局部静态变量,局部静态变量在程序第一次经过对象定义语句时初始化,并且直到程序终止时才会销毁。在此期间即使对象所在的函数结束执行也不会对它有影响。它与全局变量的生命周期相同,只是它的变量名被限定在了函数内部(关于什么时候为它分配内存,什么时候销毁的详细内容,也可以参考鄙人曾今写过的关于static的汇编分析)参数传递参数传递主要有值传递、指针传递和引用传递值传递:将实参的值拷贝到形参,然后执行函数,函数中对形参的改变不影响函数外的实参指针传递:指针值本身也是一个拷贝,在函数中可以通过对指针进行解引用操作来间接的改变函数外的实参引用传递:引用本身是对象的别名,可以在函数中通过对引用的修改,来修改函数外实参的值(其实本质上也是通过指针来进行修改)根据这几种传参方式,我们总结出来这样几点:需要改变实参的值,只能传递指针或者引用由于存在值拷贝,所以在传递大的结构体的时候尽量传递结构体的指针或者引用,如果不想修改结构体的值,可以将形参定义为const型函数通过return语句只能返回一个值,如果要返回多个值,可以使用指针或者引用。return 语句本身会进行拷贝,并且在赋值给外部变量时也会进行拷贝,尽量返回4或者8个字节的结构,对于大的结构体尽量使用引用来返回当形参有顶层const时,传给它常量对象或者非常量对象都是可以的。int func(const int i); int func(int i);由于顶层const被自动忽略,所以在上面代码会报错,两个函数的名称、形参列表实际上是相同的。数组形参除了上述这样常规的参数传递,函数中也可以传递数组,这个时候数组会自动退化为指针,例如面试或者笔试题中,经常会问到的一个问题size_t size_arr(int[10] i) { return sizeof(i); }这个时候,如果传递数组的首地址,在函数中会退化为指针,所以实际计算的是int*指针所占的内存。根据平台的不同,指针大小为4字节(32位版本)或者8字节(64位版本)如果想要真正的传递数组,可以使用引用的方式size_t size_arr(int (&arr)[10]) { return sizeof(arr); }此时arr表示有10个int型数据的数组的引用,最终得到的结果应该是 sizeof(int) * 10由于传递数组名时,数组名会退化为指针,所以如果只传递数组名,则在函数中无法确定数组的大小,为了解决这个问题,一般有3种方案:使用特殊标记,表示数组的结尾,一般字符串会这么干传递两个指针,表示数组的首地址和尾部地址,可以使用标准库中的begin 和 end 函数分别获取数组的首地址与尾地址显式传递一个表示数组元素个数的形参。这种情况一般使用下标运算,当下标达到这个指定值时退出循环当我们传递的是多维数组时,按照两个思路进行分析,多维数组其实是数组的数组,传递数组名实质上是数组的首元素地址。根据这两个原则进行分析,在传递多维数组时,后面的维度是数组元素类型,不能省略。而真正传递的是第一个该类型元素的地址。void print(int (*matrix)[10]); void print(int matrix[][10], int rowSize); //等价定义上述定义中,数组的第一维被忽略掉,第二维是10个整数的数组。上面的两个定义其实是等价的可变形参与以往使用 ... 来表示可变形参不同的是,在c++ 中新增了一个名为initializer_list 的标准库类型,这个类型只能处理所有实参类型相同的情况,对于实参类型不同的情况,可以使用可变参函数模板。initializer_list 本身是一个类似与list的结构,但是与list不同的是,initializer_list中的对象永远是常量,无法修改该容器中的值,换一个角度来说,也就无法修改实参的值了。void error_msg(initializer_list<string> il) { for(auto beg = il.begin(); beg != il.end(); ++beg) { cout << *beg << " "; } cout << endl; } //由于它是一个容器,所以在传递值的时候应该使用一对花括号把所有值括起来 if(expected != actual) { error_msg({"functionX", "expected", "actual"}); }else { error_msg({"functionX", "okay"}); }返回类型和return语句函数根据返回值类型可以分为三大类,无返回值的函数、返回普通值的函数、返回指针或者引用的函数。无返回值的函数无返回值的函数不要求非要有return 语句,这类函数在最后一句执行完后会隐式的执行return语句如果无返回值的函数需要在中间位置提前退出的话,可以使用return语句另一个使用return的场景是,直接在return后面加上函数调用,不过被调用的函数需要也是无返回值的函数返回普通值的函数有返回值的函数,必须使用return 返回一个值,返回的值必须与函数的返回类型相同,或者能隐式的转化成函数的返回值要注意保证所有路径都有返回值,一般编译器能发现这类情况,但是有的编译器不能,如果执行了没有返回值的分支,将产生未定义错误不要返回局部对象的指针或者引用函数的调用优先级与点运算和箭头运算的优先级相同,并且也附和左结合律函数的返回类型决定函数调用是否是左值,调用一个返回引用的函数得到一个左值,其他返回类型得到右值,我们能为返回类型是非常量引用的函数结果赋值当返回一个容器时,c++ 11开始,可以返回由大括号组成的初始化列表针对main函数来说,最后可以不加return语句,如果最后没有return 则编译器默认给它加上一个return 0返回数组指针的函数因为数组不能被拷贝,所以不能直接返回数组,不过可以返回数组的指针或者引用定义指向数组的指针采用的是int (*p)[10]; 的方式,同样的定义返回数组指针的函数,只需要把p定义为函数形式即可:int (*func(int i))[10];。上述定义写起来比较麻烦,而且也不容易理解,因此可以使用类型别名的方式来简化定义方式int odd[] = {1, 3, 5, 7, 9}; int even[] = {2, 4, 6, 8, 10}; decltype(odd)* arrPtr(int i) { return (i % 2) ? &odd, &even; }当我们直到返回的数组指针具体指向哪个数组,可以使用decltype关键字来声明函数的返回类型。从c++11 开始,提供了一种新的定义方式,即尾置返回类型的方式auto func(int i) -> int (*)[10];函数重载c++ 与 c语言中的一个很大的不同就是c++ 允许函数重载。同一作用域内的几个函数名称相同,但形参列表不同的,称之为函数重载。注意这里的几个前提条件:同一作用域、函数名称相同、形参列表不同;这些条件缺一不可。而且这里说的是形参列表不同,返回值不同的不能算是重载。main函数作为入口函数,只能有一个顶层const不影响传入的参数,因此认为顶层const与普通形参相同,不认为是重载如果传入的参数是引用或者指针,可以根据它所指向的对象是否为const来进行区分,所以底层const可以作为重载由于非const型参数能转化为const型,所以当传参中多个函数都满足,编译器会优先选择const版本在实际使用时,根据调用时的传参,来与一组重载函数中的某一个关联起来,这个过程叫做函数匹配或者叫做重载确定一般情况下函数匹配过程很容易分别出来,要么是形参个数不同,要么是类型毫无关系,但是也有例外,例如:形参中存在默认值形参中一种类型可以转化为另一种类型目前来说调用函数的时候会出现下列三种情况:可以从一堆重载函数中正确匹配,编译通过没有函数复合调用时传入的实惨,此时编译报错,无法找到对应函数多个重载形式都复合传入的实惨,此时编译报错,存在二义性不要在局部作用域中定义函数,因为局部作用域内出现重名情况时,会进行名称覆盖特殊用途的语言特性默认实参在定义函数时,对于后续多次调用时,传入相同实参值的形参,可以给予一个默认值。这样在调用这个函数时,针对提供了默认值的参数,可以传参也可以不传函数调用时按照实参位置解析,默认实参负责填补函数调用缺少的尾部实参内联函数一般函数调用涉及到参数的拷贝,返回值的拷贝,以及最终栈的回收等一系列操作。调用函数存在一定的性能开销,因此为了提高性能或者提高代码的重复使用率,c中可以使用宏定义来定义一个短小的常规操作,最终编译时会被编译器展开。但是宏定义无法对传入参数进行校验,而且需要注意的问题较多,不好理解。C++中引入内联函数,它与宏的功能类似,是一种没有额外开销的函数一般在函数的返回值类型前面加上inline 关键字就定义了一个内联函数并不是所有的函数都可以定义为内联函数。内联函数用于优化规模小、流程直接、调用频繁的函数,很多编译器不支持内联递归函数。而且一个大于75行的函数也不大可能在调用点内联展开constexpr 函数constexpr 函数是指能用于常量表达式的函数。constexpr函数与普通函数的定义相同,不过要遵循几项约定:函数的返回值以及所有形参类型都是字面值类型函数体中必须有且只有一条return语句在编译阶段,constexpr函数会被直接替换为它返回的具体的值,为了便于函数正常展开,constexpr函数默认都是内联函数由于在编译阶段编译器需要知道内联函数和constexpr 函数的定义。因此这两种函数可以重复定义。但是定义时要保证内容完全相同,基于这个理由,可以将这两种函数统一放到一个头文件中,在需要使用的时候包含它调试帮助可以使用assert预处理宏与NODEBUG宏,其中assert只有在调试模式下才会起作用,而NODEBUG宏则表示当前在发布模式下,此时assert函数不会起作用另外C++ 也定义一些名字便于调试:__FILE__: 当前代码所在文件的名称__LINE__: 当前代码所在行数__TIME__: 当前代码文件被编译的时间__DATE__: 当前代码文件被编译的日期__func__: 当前代码所在的函数函数匹配在大多数情况下,很容易分辨某次调用应该选择哪个重载函数,然而当几个重载函数的形参数量相等以及某些形参的类型可以由其他类型转化得来时,这项工作就不那么容易了。void f(); void f(int); void f(int, int); void f(double, double = 3.14); //调用 f(5.6);函数匹配过程一般经历如下步骤:确定候选函数和可行函数第一步选定本次调用对应的重载函数集,集合中的函数被称之为候选函数。候选函数具备两个特点,一是与被调用的函数同名,二是其声明在调用点可见,这步下来,上述例子中所有f函数都满足条件第二步考察本次调用提供的实参,然后从候选函数中选择能被这组实参调用的函数,这些函数被称为可行函数,可行函数也有两个特征,一是其形参数量与本次调用提供的实参数量相同,二是每个实参与对应形参类型相同,或者能转化成形参的类型。上述实例,调用传入的是一个double类型的参数,double可以转化为int,因此这个时候发现满足条件的是 void f(int); 和 void f(double, double=3.14);寻找最佳匹配第三步是从可行函数中寻找与本次调用最匹配的函数,它的基本思想是实参类型与形参类型越接近,它们匹配的越好。如果多个形参都与调用函数的实参较为接近且,如果有且只有一个函数同时满足下面两个条件,则匹配成功:该函数每个实参的匹配不劣与其他可行函数需要的匹配至少有一个实参的匹配优于其他可行函数提供的方案如果检查了所有实参后没有任何一个函数脱颖而出,则调用错误,编译器将报告二义性。调用重载函数尽量避免强制类型转换,如果在实际应用中需要进行强制类型转换,说明我们设计的形参集合不合理分析上面的例子,如果采用 void f(int); 在调用时会进行一次将double转化为int的类型转化,如果使用 void f(double, double=3.14); 5.6作为double的第一个参数进行传递不需要类型转化,而第二个参数使用默认形参,这里可以不传,因此相比较与第一种int的传参方式,后一种显然更加复合实参类型转化为了确定最佳匹配,编译器将实参类型到形参类型的转化划分为几个等级,具体排序如下所示:精确匹配,包括下列情况 1.1. 实参类型和形参类型相同 1.2. 实参从数组类型或者函数类型转化为对应的指针类型 1.3. 像实参添加顶层const或者从实参中删除顶层const通过const转换实现的类型匹配通过类型提升实现的类型匹配通过算术类型转换或者指针转换实现的匹配通过类类型转换实现的匹配函数指针声明函数指针时,只需要将函数声明中的函数名写为指针名即可,但是需要注意使用括号将表示指针的*与指针名称括起来void (*f)(int);当我们把函数名直接作为一个值使用时,该函数自动转化为指针;也可以使用取地址符针对函数名称取地址,二者是等价的。指向不同类型函数的指针不存在类型转化重载函数的指针必须与某个函数精确匹配,不存在形参类型转化之类的规则可以使用typedef来为函数指针类型定义一个类型别名typedef void(*f)(int); //将返回void、传入一个int参数的函数指针取类型别名为f
2021年03月09日
0 阅读
0 评论
0 点赞
2021-03-01
c++基础之语句
上一次总结了一下c++中表达式的相关内容,这篇博文主要总结语句的基础内容简单语句c++ 中语句主要是以分号作为结束符的,最简单的语句是一个空语句,空语句主要用于,语法上需要某个地方,但是逻辑上不需要;最常见的就是循环里面复合语句是用大括号括起来的语句块叫做复合语句,复合语句也叫做块。一个块就是一个作用域,在块中引入的名字只能在块内部以及嵌套在块里面的子块中访问。通常名字在有限的区域内可见,该区域从名字定义开始,到名字所在块的结尾为止。语法上需要一条语句,但是逻辑上需要多条语句的,应该使用语句块,例如if或者while等循环里面。块不以分号结束。{};, 算两条语句空块是指内部没有任何语句的一对花括号语句作用域语句中变量的作用域只在当前语句块中有效,如果其他代码也想访问控制变量,则变量必须定义在语句块的外部。条件语句条件语句需要注意:if语句每个分支尽量加上大括号,即好读,也能避免很多问题switch 语句中case后面必须跟上整型常量表达式一般不要省略case分支最后的break语句,如果是特殊逻辑需要这么做的,使用注释进行说明即使不准备在default分支中做任何事,最好也写上default分支。其目的在于告诉程序的读者,我们已经考虑到了默认的情况,只是目前什么也没有做要在case分支中定义变量应该定义在大括号中,并且只在当前分支中使用它迭代语句迭代语句又叫做循环语句,一般有while、do while、for三种形式这些语句一般的语言中都有,这里就不多做介绍,主要介绍c++ 11中新增的一种范围for的形式范围for可以遍历容器或者其他序列的所有元素,它的简单形式是for(declaration: expression) statementdeclaration 定义一个变量,序列中的每一个元素要都能转化为该变量的类型,然后执行拷贝操作,将每次迭代的值拷贝到该变量中。变量只是序列中元素的拷贝,无法修改元素的值,如果想要修改元素的值,需要将变量定义为引用类型。statement 是一个语句或者语句块,所有元素都处理完后,循环结束跳转语句跳转语句主要有break、continue以及goto语句。break:用于跳出离它最近的while、do while、for或者switch语句,并从这些语句之后的第一条语句开始执行。continue:终止最近的循环语句中当前迭代并立即进入下一次迭代,它只能出现在循环语句中。goto:跳转到对应标签处,标签可以定义在函数任意位置。注意只能作用于函数内部,不能由一个函数跳转到另一个函数。尽量少用goto,因为它可读性差,而且不好控制。异常处理语句c++中的异常处理包括这样几个部分:throw表达式:用于抛出一个异常try: 异常处理部分使用try语句块处理异常,try语句块以关键字try开始,并以一个或者多个catch子句结束。try语句块中代码抛出的异常通常会被某个catch子句处理。因为catch子句处理异常,所以它们也被称之为异常处理代码异常类:用于在throw表达式和相关的catch子句之间传递异常的具体信息throw 后面跟一个表达式,表达式返回值的类型就是抛出异常的类型。跟在try 语句块之后的是一个或者多个catch子句,当try中的异常与某一个catch中捕获的异常类型匹配,则执行该catch块中的内容。注意try块与catch子句是两个语句块,在try中定义的变量无法在catch块中使用。标准异常库标准异常库被分别定义在4个头文件中:exception 头文件定义了最为通用的异常类exception。它只报告异常的发生,不提供任何额外信息stdexcept 头文件中定义几种常见的异常类new 头文件中定义了bad_alloc 异常type_info 头文件定义了bad_cast 异常类型在stdexcept 头文件中定义的异常类主要有:exception: 最常见的问题runtime_error: 只有在运行时才能检测出来的问题range_error: 运行时错误,生成的结果超出了有意义的值域范围overflow_error: 运行时错误,计算上溢underflow_error: 运行时错误,计算下溢logic_error: 程序逻辑错误domain_error: 逻辑错误,参数对应的结果值不存在invalid_argument: 逻辑错误,无效的参数length_error: 逻辑错误,试图创建一个超出该类型最大长度的对象out_of_range: 逻辑就错误,使用一个超出有效范围的值标准库异常类只定义了几种运算,包括创建或者拷贝异常类型的对象,以及为异常类型的对象赋值异常类型中只定义了一个名为what的成员函数,返回值为const char* 的c风格的字符串,该字符串的目的是提供关于异常的一些文本信息。
2021年03月01日
1 阅读
0 评论
0 点赞
2021-02-03
c++基础之表达式
这次接着更新《c++ primer》 这本书的读书笔记,上一篇博文更新到了书中的第三章,本次将记录书中的第四章——表达式左值与右值在理解表达式之前需要先理解c++中左值和右值的概念。c++ 的表达式要么是右值,要么是左值,这两个名词是从c语言中继承过来的,在c语言中,左值指的是可以位于赋值语句左侧的表达式,右值则不能。在c++中二者的区别就相对复杂一些了。在c++要区分左值和右值,可以采取一个原则:一般来说当一个对象被用作左值时,用的是对象的地址,也就是在内存中的位置,而右值可以采取排他性原则,只要不是左值的都是右值。不同运算符对运算对象的要求各不相同,有的要求左值、有的要求右值;返回值也有差异,有的作为左值返回,有的作为右值返回。一个重要的原则是:凡事需要右值的地方可以使用左值来代替,但是不能把左值当成右值来使用。一般下列运算符需要用到左值赋值运算符的左侧需要一个左值。返回的结果也是一个左值取地址运算符作用于一个左值运算对象,返回一个指向该对象的指针,结果是一个右值内置解引用运算符、下表运算符迭代器解引用运算符、string、vector的下标运算符的求值结果都是左值内置类型和迭代器的递增递减运算符作用于左值对象,其前置版本所得到的结果也是左值优先级与结合律复合表达式是指含有两个或者多个运算符的表达式,计算复合表达式的值需要将运算符和运算对象合理的组织在一起,优先级与结合律决定了运算对象的组合方式。表达式中的括号无视运算优先级与结合律的规则,如果表达式中有括号,先运算括号中的内容。表达式的最终值取决与子表达式的结合方式,在计算表达式的值时,先看运算符的优先级,先处理优先级高的子表达式,而优先级相同的情况下,则由其结合律规则决定3 + 4 * 5 //根据运算符的优先级,乘法高于加法,所以先计算4 * 5 为20,再计算3 + 20 得到23 20 - 15 - 3 //先看运算符的优先级,都是减法优先级相同,再看结合律,减法的结合律是从左到右,所以先计算20 -15 得到 5,然后再计算5 - 3 得到2 6 + 3 * 4 / 2 + 2 //先看运算符的优先集,乘法除法的优先级大于加法,而乘法除法的结合律都是从左到右结合,所以这个表达式先计算 3 * 4 得到12,再计算 12 / 2 得到 6 ,最后加法的结合律也是从左到右,最后计算 6 + 6 + 2 得到 14求值顺序优先级规定了运算对象的组合方式,但是并没有规定运算对象按照什么顺序求值,在大多数情况下不会明确指定求值顺序。例如在表达式 int i = f1() * f2(); 中,先计算函数的返回值,然后再将结果赋值进行乘法运算,最后将结果赋值给i变量,但是究竟是先计算f1函数还是先计算f2函数,这个c++标准没有明确规定。对于没有指定执行顺序的运算符来说,如果表达式指向并修改了同一个对象,将会引发错误并产生未定义的行为,例如int i = 0; int j = i + ++i;根据结合律,会先计算i和 ++i但是不确定是该先计算i还是先计算++i 这里会产生未定义行为。如果先计算i则表达式可以转化为 j = 0 + 1 如果先计算 ++i,则表达式可以转化为 j = 1 + 1;有4中表达式明确规定了求值顺序逻辑与(&&):只有当左侧的结果为真时,才计算右侧的结果逻辑或(||):只有当左侧的运算结果为假时,才会计算右侧结果三目运算符(?:)当条件为真时,计算:左侧的表达式,否则计算右侧的表达式逗号表达式:运算顺序是从左到右,最后返回最右侧的表达式的值在处理复合表达式时,有下面两条准则:在不清楚运算对象的优先级和结合律的时候,按照实际的结合逻辑使用括号如果改变了某个运算对象的值,在表达式的其他地方不要使用这个运算对象,但是能明确知道求值顺序的时候这个规则就不适用了算术运算符算术运算符的求值对象和求值结果都是右值。算术运算符的优先级顺序为:单目运算符(+表示取当前值,-表示取相反数) > 乘除法 > 加减法;结合律:采用从左至右结合的方式算术运算符能作用与所有的算术类型,算术类型的数据在运算前会被转化为精度较大的类型(运算对象只有byte,char, short时会被统一转化为int),在转化为同一类型后执行再进行运算bool b = false; int k = 1; bool i = +k + -b;在上述代码中,bool类型参与算术运算时,会将true变为1,false变为0,然后针对0和1进行操作,根据优先级得到 i = 1 + 0; 最后再将算术类型转化为bool类型赋值,i最终为true除法运算中如果除数和被除数符号相同,商为正数,否则为负数,c++11 标准中规定负数商一律向0取整取余运算,要求除数和被除数都是整数,如果m/n的结果不为0,则m%n的结果符号与m相同(m/n)*n + m%n = m(-m)/n=m/(-n)=-(m/n)(-m)%n=-(m%n); m%(-n)=m%n逻辑运算符逻辑运算符作用与任何能转化为boo类型的运算对象上优先级为 逻辑非 > 大于/小于/大于等于/小于等于 > 相等/不等 > 逻辑与 > 逻辑非逻辑运算符一般的语言中都有,而且用法基本类似,这里就不再详细说明了,需要注意的是:使用非bool类型来做判断时,不要写成 if(!val) 或者 if(val == true);同样的使用bool类型来判断时,也不要写成 if(val == true) 或者 if(val == 1)在进行数值相等的比较时,为了防止少写=,习惯上把常量写在前面例如 if(1 == val)赋值运算符赋值运算符一般作用与初始化给对象赋值或者在后续修改对象的值,但是需要注意区分二者的不同,这点在初始化或者给类对象赋初始值的时候尤其重要赋值运算符的左侧必须是一个可修改的左值。赋值运算符的结果是它左侧的运算对象,并且是一个左值。结果的类型就是左侧运算对象的类型,如果赋值运算符左右两个运算对象的类型不同,则运算对象将转化成左侧运算对象的类型。int i, j; i = j = 10; const k = 10; //这里是初始化,不是赋值 k = i; //错误,左侧需要可以修改的左值新的c++ 标准中允许使用初始化列表来给对象进行赋值i = {3.14}; //错误,使用初始化列表时,不能出现精度丢失 i = 3.14; //正确,值为3 vector<int> vi; vi = {0, 1, 2, 3, 4, 5};对于内置类型,初始化列表赋值时,列表中最多只能有一个值,而且值的精度不能大于左侧对象的精度赋值运算符满足右结合律,对于多重赋值语句中的每一个对象,它的类型或者与右边的对象相同,或者可以又右边对象的类型转化得到赋值运算符的优先级较低赋值运算符也包括复合赋值运算符,例如 += 、-=、*= /=递增和递减运算符递增和递减运算符为对象的加一和减一提供了一种简洁的书写形式。这两个运算符还可以应用于迭代器。递增和递减运算符有前置版本和后置版本,前置版本是先加一,然后将改变后对象的值作为求值结果;后置版本是先将对象的结果作为求值结果返回,然后再改变对象的值。在性能上,在涉及复杂的迭代运算时,前置版本会大大优于后置版本,因此尽量养成使用前置版本的习惯。auto pbeg = v.begin() while(pbeg != v.end() && *pbeg >= 0) { cout << *pbeg++ << endl; }这里后置递增运算符的优先级要大于解引用的优先级,所以这里等价于 *(pbeg++),即先进行后置递增运算,但是返回变化之前的迭代器,然后将变化之前的迭代器进行解引用操作,得到具体元素的值递增和递减运算符可以修改对象的值,而一般的运算符没有严格规定求值的顺序,所以在复合表达式中需要额外注意,不要在可能修改变量值的位置访问该变量string s = "hello world"; auto beg = s.begin(); while(beg != s.end() && !isspace(*beg)) { *beg = toupper(*beg++); }上述例子由于赋值运算符未定义两侧运算对象的求值顺序,可能先求值左侧,那么循环中的语句等效于 beg = toupper(beg); 如果先求值右侧,则等效于 (beg + 1) = toupper(beg);条件运算符条件运算符也叫做三目运算符。cond ? expr1:expr2;条件运算符也可以嵌套使用, 条件运算符满足右结合律。随着嵌套层数的增加,代码的可读性极具下降,因此条件运算的嵌套最好不要超过三层。条件运算符的优先级非常低,一般使用的时候建议加上括号cout << ((grade > 60) ? "pass" : "fail"); // 输出pass 或者 fail cout << (grade > 60)? "pass" : "fail"; // 输出 1或者0,运算结果 是 "pass" 或者 "fail" cout << grade > 60 ? "pass" : "fail"; // 试图将cout 与 60 进行比较,错误位运算符位运算是作用与对象的二进制值的,理论上它可以处理任何对象,但是为了代码安全和可读性,建议只处理整型数据,而且最好是无符号整型运算符功能用法~按位求反~expr<<左移expr << expr2>>右移expr >> expr2&位与expr & expr2^位异或expr ^ expr2\ 位或exprexpr2sizeof 运算符sizeof 返回一个类型或者一个表达式所占的字节数。它满足右结合律针对表达式,sizeof并不计算表达式的值,只返回表达式结果类型的大小由于sizeof 不计算表达式的值,因此即使在sizeof中解引用指针也不会有什么影响逗号表达式逗号运算符含有两个表达式,按照从左至右的顺序依次求值逗号表达式先对左侧表达式进行求值,然后丢弃返回的结果,然后再对右侧表达式进行求值。逗号表达式的返回值是右侧的表达式的值类型转换何时发生隐式转换大多数情况下,比int小的整型值会被转化为int条件中,非布尔值会被转化为布尔类型初始化过程中,初始值转化为变量类型;赋值语句中右侧运算对象转化成左侧运算对象的类型如果是算术运算或者关系运算的运算对象有多种类型,需要转化为同一种类型。而且会尽量往精度较大的一方转化调用函数时也可能会发生类型转化算术类型转换算术转换总是朝着精度更高的一级转换较小的整型会被转化为int,较大的整型会被转化为long、unsigned long、unsigned longlong 等其他隐式类型转换除了算术类型的隐式转换外,还有下面几种数组转化为指针:当数组被用作 decltype、sizeof、取地址符一级typeid 等运算符的运算对象时,该转换不会发生指针的转化:常量整数0和nullptr可以转化为任意类型的指针,指向任意非常量的指针能转化成void,指向任意对象的指针能转化为const void转化为布尔类型: 算术类型或者指针,值为0或者nullptr的被转化为false,其他的值被转化为true转化为常量:常量的指针或者引用可以指向非常量对象,反过来则不行;类类型定义的转化:由程序员预先定义,在需要转化时,由编译器自动调用进行转化显式类型转换显式类型转换绕过了编译器的类型检查,是不安全的一种转化方式显示类型转换的语法规则如下:cast-name<type>(express);其中type是目标类型,express是要转化的值,如果type是引用类型则结果是一个左值。cast-name是 static_cast、dynamic_cast、const_cast 和 reinterpret_cast 中的一种static_cast 只要不包含底层const,都可以使用static_cast,在对指针进行强制类型转化时,要保证转化前与转化后指针所指向的对象类型相同,用于同类型数据之前的转化,如算术类型之前的相互转化。const_cast 只能改变运算对象的底层const、与static_const互相补充reinterpret_cast 重新解释比特位,通常为运算对象的位模式提供较低层次上的重新解释。一般用于指针之间的转化,它没有限制,任何类型间都可以进行转化。但是也十分危险dynamic_cast 动态类型转化,主要用于多重继承类类型之间的转化
2021年02月03日
3 阅读
0 评论
0 点赞
2021-01-24
c++基础之字符串、向量和数组
上一次整理完了《c++ primer》的第二章的内容。这次整理本书的第3章内容。这里还是声明一下,我整理的主要是自己不知道的或者需要注意的内容,以我本人的主观意志为准,并不具备普适性。第三章就开始慢慢的接触连续、线性存储的数据结构了。字符串、数组、vector等都是存储在内存的连续空间中,而且都是线性结构。算是c++语言中的基础数据结构了。命名空间与using使用方式如下using namespace::name;其中name表示命名空间的具体名字如标准库都在std 这个命名空间,如果要引用这个命名空间的内容就写作 using namespace::std;另外namespace可以表示作为关键字,也可以作为具体的命名空间,如果作为具体命名空间的话,name此时应该是命名空间中的类或者函数等等成员,例如要引用cin这个函数的话,可以这样写 using std::cin在使用时除了使用命名空间之外也可以直接带上命名空间的名称,例如要使用cout 做输出时可以这么写 std::cout << "hell world" << std::endl;使用using 可以直接引入命名空间,减少代码编写的字符数,但是当引入多个命名空间,而命名空间中又有相同的成员时,容易引发冲突。所以在使用命名空间时有下面几条建议头文件中不要包含using声明尽量做到每个成员单独使用using声明string 对象定义和初始化string对象初始化string对象有如下几种方式:string() : 初始化一个空字符串string(const string&): 使用一个字符串来初始化另一个字符串,新字符串是传入字符串的一个副本string(char*): 使用一个字符数组来初始化字符串string(int, char): 新字符串是由连续几个相同字符组成需要注意的是,在定义的语句中使用赋值操作符相当于调用对应的初始化语句。而在其他位置使用赋值操作符在执行复写操作string str = "hello world"; //此处调用拷贝构造,并没有调用赋值重载函数string 对象的操作string的操作主要有:os << s: 将s的值写入到os流中,返回osis >> s: 从is流中读取字符串,并赋值给s,字符串以空白分分隔,返回isgetline(is, s): 从is中读取一行,赋值给s,返回iss.empty(): 判断字符串是否为空,为空则返回true,否则返回falses.size(): 返回字符串中字符个数, 类型为string::size_type。它是一个无符号类型的值,而且编译器需要保证它能够存放任何string对象的大小。不要使用size()的返回值与int进行混合运算s[n]: 返回第n个字符s+s1: 返回s和s1拼接后的结果s1=s2: 将s2的值赋值给s1,执行深拷贝s1 == s2: 判断两个字符串是否相等s1 != s2:判断两个字符串不等<, <=, >, >=:字符串比较处理string 中的字符string 本身是一个字符的容器,我们可以使用迭代的方式来访问其中的每一个字符。例如// 字符转化为大写 string s = "hello world"; for(auto it = s.begin(); it != s.end(); it++) { *it = toupper(*it); }针对这种需要在循环中迭代访问每个元素的情况,c++针对for语句进行扩展,使其能够像Java等语言那样支持自动迭代每一个元素,这种语句一般被称之为范围for。// 统计可打印字符 string s = "hello world"; int punctt_count = 0; for(auto c : s){ if(ispunct(c)){ ++punct_count; } }上述代码中c 只是s中每一个字符的拷贝,如果想像之前那样修改字符串中的字符,可以在迭代时使用引用类型//字符串转化为大写 s = "hello world"; for(auto& c : s){ c = toupper(c); }所有同时具有连续存储和线性存储两个特点的数据结构都可以使用下标访问其中的元素。字符串中字符是采用线性和连续存储的。所以这里它也可以采用下标运算符// 字符串转化为大写 string s = "hello world"; for(auto index = 0; index < s.size(); ++index) { s[index] = toupper(s[index]); } 在使用下标时需要注意下标是否超过容器中存储的元素个数。由于在编译与链接时不会检查这个,如果超出在运行时将会产生未定义结果。标准库 vector标准库vector 表示对象的集合,里面需要存储相同类型的对象。可以看作是一个动态数组。vector 被定义在头文件 vector中由于vector中存储的是对象,而引用不是对象,所以不存在存储引用的vector定义和初始化除了可以使用与string相同的初始化方法外,新的标准还支持使用初始化列表来初始化vectorvector<string> vec = {"Hello", "World", "Boy", "Next", "Door"};一般来说都是预先定义一个空的vector对象,在需要的时候使用push_back或者push_front添加元素。需要注意的是在使用迭代器的过程中,不要针对容器做删减操作同样的vector可以使用下标来访问元素,但是需要注意下标只能访问已有元素不能使用下标来添加元素,同时使用下标时需要注意范围。访问超过范围的元素,会引起越界的问题迭代器迭代器是一组抽象,是用来统一容器中元素访问方式的抽象。它能保证不管什么类型的容器,只要使用迭代器,就能使用相同的方式方法从头到尾访问到容器中的所有元素。在这里不用过于纠结跌打器究竟是如何实现的,只需要知道如何使用它。另外提一句,我当初在初学的时候一直把c语言的思路带入到c++中,导致我一直认为跌迭代器就是指针或者下标,我试图使用指针和下标的方式来理解,然后发现很多地方搞的很乱,也很模糊。这个概念我是一直等待学习python和Java这种没有指针、完全面向对象的语言之后,才纠正过来。这里我想起《黑客与画家》书中提到的,编程语言的高度会影响我们看待问题高度。从我的经历来看,我慢慢的理解了这句话的意思。所以这也是我当初学习lisp的一个原因。我想看看被作者称之为数学语言,抽象程度目前最高的语言是什么样的,对我以后看问题有什么影响迭代器提供了两种重要的抽象:提供统一的接口来遍历容器中所有元素;另外迭代器提供统一接口,让我们实际操作容器中的元素使用迭代器迭代器的使用如下:迭代器都是使用begin 获取容器中的第一个元素;使用end获取尾元素的下一个元素迭代器自身可以像操作对象的指针一样操作容器中的对象迭代器比较时,比较的是两个迭代器指向的是否是同一个元素,不支持 >、<比较++ 来使迭代器指向容器中下一个位置的对象,--来指向上一个位置的对象如果不想通过迭代器改变容器中元素的值,可以使用const类型的迭代器,即 const_iterator类型的迭代器#+BEGIN_SRC c++ string s = "Hello World"; for(string::const_iterator it = s.begin(); it != s.end(); it++) { cout << *it << endl; } #+END_SRCbegin 和end返回的是普通类型的迭代器,c++ 11中提供了一套新的方法来获取const类型的迭代器,cbegin和 cend迭代器的常见运算迭代器常见运算:iter + n: 迭代器向前可以加上一个整数,类似于指针加上一个整数,表示迭代器向前移动了若干个元素iter - n: 迭代器往前移动了若干个元素,类似于指针减去一个整数iter1 - iter2: 表示两个迭代器之间的间距,类似于指针的减法、<、>=、<=:根据迭代器的位置来判断迭代器的大小,类似于指针的大小比较迭代器与整数运算,如果超过了原先容器中元素的个数,那么最多只会返回容器中最后一个元素的下一个跌打器,也就是返回值为 end函数的返回迭代器相减得到迭代器之间的距离,这个距离指的是右侧的迭代器移动多少个元素后到达左侧迭代器的位置,其类型定义为difference_type使用迭代器来访问元素时,与使用指针访问指向的对象的方式一样,它重载了解引用运算符和箭头运算符。使我们能够像使用指针那样使用迭代器数组数组与vector相似二者都是线性存储二者存储的都是相同类型的元素与vector不同的是:数组大小固定由于大小在初始化就已经确定,所以在性能上优于vector,灵活性上有些不足定义和初始化内置数组在初始化数组的时候需要注意:数组大小的值可以是字面值常量、常量表达式、或者普通常量定义数组时必须指明类型,不允许用auto由初始化值来进行推断const unsigned int cnt = 42; //常量 constexpr unsigned int sz = 42; //常量表达式 int arr[10]; //使用字面常量指定大小 int arr2[cnt]; //使用常量初始化 int arr3[sz]; //使用常量表达式初始化可以在初始化时不指定大小,后续会根据初始化列表中的元素个数自动推导出数组大小同时指定了数组大小和初始化列表,如果指定大小大于初始化列表中的元素个数,那么前面几个元素按照初始化列表中的值进行初始化,后面多余的元素则初始化为默认值如果指定大小小于初始化列表中元素个数,则直接报错const unsigned int sz = 3; int arr1[sz] = {1, 2, 3}; int arr2[sz] = {1}; // 等价与 arr2[sz] = {1, 0, 0} int arr3[] = {1, 2, 3}; int arr4[sz] = {1, 2, 3, 4}; //错误,初始化列表中元素个数不能大于数组中定义的元素个数字符数组可以直接使用字符串常量进行赋值,数组大小等于字符串长度加一我们可以对数组中某个元素进行赋值,但是数组之间不允许直接进行拷贝和赋值和vector中一样,数组中存储的也是对象,所以不存在存储引用的数组。在理解关于数组的复杂声明时,采用的也是从右往左看理解的方式。或者说我们先找到与[] 结合的部分来理解,与[]结合的部分去掉之后就是数组中元素的类型。int * ptrs[10]; int & refs[10]; int (*Parry)[10]; int (&arrRef)[10];上面的例子中:ptrs,首先与[]结合最紧密的是ptrs 去掉这两个部分,剩下的就是int 这部分表示数组中元素类型是int , 也就是这里定义了一个包含10个int指针元素的数组refs, 首先与[]结合最紧密的是ref2,去掉这个部分,剩下的就是int&,这部分表示数组中元素类型是int&,也就是这里定义了一个包含10个指向int数据的引用的数组,由于不存在存储引用的数组,所以这里是错误的Parry,由于有了括号,与[]结合最紧密的就变成了 int,也就是我们先定义了一个包含10个int类型的数组,而Parry本身是一个指针,所以这里定义的其实是一个指向存储了10个int类型数据的数组的指针同样的方式分析,得到arrRef 其实是一个指向存储了10个int类型数据的数组的引用指针和数组在上面的例子中,已经见过了指针和数组的一些定义方式,例如ptrs 是一个存储了指针的数组,这种数组一般称之为指针数组;Parry是一个指向数组的指针,这种指针被称之为数组指针在某些时候使用数组的时候,编译器会直接将它转化为指针,其中在使用数组名时,编译器会自动转化为数组首元素的地址。int ia[] = {1, 2, 3, 4, 5}; auto ia2 = ia; ia2[2] = 10; // 这里ia2是指向ia数组首元素的指针,这里其实是在修改ia第3个元素的值需要注意的是在使用decltype时,该现象不会发生,decltype只会根据表达式推断出类型,而不会具体计算表达式的值,所以它遇到数组名时,根据上下文知道它是一个数组,而不会实际取得数组首元素的地址int ia[] = {1, 2, 3, 4, 5}; decltype(ia) ia2 = {0}; //这里ia2 是一个独立的数组,与ia无关 ia2[2] = 10;指针也可以看作迭代器的一种,进行迭代时终止条件是数组尾元素下一个位置的地址int ai[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; int *pbegin = &ai[0]; int *pend = &ai[10]; for(int* it = pbegin; it != pend; it++) { cout << *it << endl; }c++ 11中引入两个函数来获取数组的begin位置和end位置,分别为begin() 与 end()int ai[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; for(int *p = begin(ai); p != end(ai); p++) { cout << *p << endl; }c 风格的字符串string转化为char 可以使用string.c_str()函数,该函数返回的是const char,以取保无法通过这个指针修改字符串本身的值,另外该函数返回的地址一直有效,如果后续修改了string的值,那么根据字符串的算法,字符串中保存字符的地址可能发生变化,此时再使用原来返回的指针访问新的字符串,可能会出现问题如果执行完c_str函数后,程序想一直访问其返回的数组,最好将该数组重新拷贝一份string s = "hello world"; const char* pszBuf = s.c_str() char* pBuff = new char[s.size() + 1]; memset(pBuff, 0x00, sizeof(char) * s.size() + 1); strcpy(pBuff, pszBuff); //后面可以直接使用pbuf,即使s字符串改变 s = "boy next door"; //do something delete[] pBuf;为了与旧代码兼容,允许使用数组来初始化一个vector容器,只需要指明需要拷贝的首元素地址和尾元素地址int arr[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ,10}; vector<int> va(begin(arr), end(arr));多维数组多维数组是数组的数组,数组中每一个成员都是一个数组。当一个数组的元素仍是数组时,需要多个维度来表示,一个表示数组本身的大小,一个维度表示元素中数组大小对于二维数组来说,一般把第一个维度称之为行,第二个维度称之为列。int ia[3][4] = { {0, 1, 2, 3}, {4, 5, 6, 7}, {8, 9, 10, 11} }; //等价于 int ia[3][4] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11};多维数组的初始化可以用打括号初始化每个维度的数据,也可以省略中间的大括号,这样它会按照顺序初始化但是需要注意int ia[3][4] = { {0}, {1, 2}, {3, 4, 5} }; int ia[3][4] = {0, 1, 2, 3, 4, 5};上述代码中,二者含义完全不一样,上一个表示每个子元素中的数组如何初始化,最终结果为{0, 0, 0, 0, 1, 2, 0, 0, 3, 4, 5, 0}。下面一个是从第一行开始依次初始化所有元素,最终结果为{0, 1, 2, 3, 4, 5, 0, 0, 0, 0, 0, 0}可以使用下标访问数组元素,一个维度对应一个下标int ai[3][4] = {0}; cout << ai[2][3] << endl; //如果下标个数和数组维度一样,将得到具体类型的值 cout << ai[2] << endl; //下标数小于数组维度,得到对应子数组的首地址可以使用for循环遍历数组int a[3][4] = {0}; for(auto row : a){ for(auto i : row) //错误不能对指针使用迭代 { cout << i << endl; } }上述例子中,由于多维数组中存储的是数组元素,所以row默认是数组元素,也就是数组首地址,是指针类型,也就不能使用内层的迭代了我们可以稍微做一些修改int a[3][4] = {0}; for(auto& row : a){ for(auto i : row) //错误不能对指针使用迭代 { cout << i << endl; } }使用引用声明之后,row就表示指向内层子数组的一个数组的引用,也就是一个子数组本身,针对数组就可以使用范围for了注意:使用for范围遍历时,除了最内层元素,其余的都需要声明为引用类型多维数组的名称也是数组的首地址定义多维数组的指针时,需要明确,多维数组是存储数组的特殊数组int ai[3][4] = {0}; int (*p)[4] = ai; // int *p[4] 表示的是指针数组,数组有4个成员,每个成员都是一个int* 上述代码,ai是一个存储3个数组元素的数组,每个元素又是存储4个整型元素的数组,因此定义它的指针的时候,需要明确,指针类型应该是数组元素的类型,也就是有4个int型元素的数组的指针当然如果嫌麻烦或者不会写,可以使用auto来定义一般来说,书写多维数组的指针是比较麻烦的一件事,可以使用类型别名让它变得简单点,上面的例子可以改写一下//typedef int int_array_4[4]; 二者是完全等价的 using int_array_4 = int[4]; int_array_4 *pArr = ai; for(; pArr != ai + 3; ++pArr) { for(int *p = *pArr; p != *pArr+4; ++p) { cout << *p << " "; } cout << endl; } 数组名代表的是数组的首元素,多维数组又可以看作是一个存储数组的数组。所以这里ai的名称代表的是一个存储了3个元素的数组,每个元素都是存储4个整型数据的数组。pArr 的类型是存储了4个整型元素的数组的指针,所以这里与ai表示的指针的类型相同。这里我们将ai的值赋值给指针。在循环中,外层循环用来找到ai数组中每个子数组的指针。内层循环中,使用pArr解引用得到指针指向的每一个对象,也就是一个存储了4个整型元素的数组。针对这个数组进行循环,依次取出数组中每一个元素。
2021年01月24日
1 阅读
0 评论
0 点赞
2021-01-17
c++基础之变量和基本类型
之前我写过一系列的c/c++ 从汇编上解释它如何实现的博文。从汇编层面上看,确实c/c++的执行过程很清晰,甚至有的地方可以做相关优化。而c++有的地方就只是一个语法糖,或者说并没有转化到汇编中,而是直接在编译阶段做一个语法检查就完了。并没有生成汇编代码。也就是说之前写的c/c++不能涵盖它们的全部内容。而且抽象层次太低,在应用上很少会考虑它的汇编实现。而且从c++11开始,加入了很多新特性,给人的感觉就好像是一们新的编程语言一样。对于这块内容,我觉得自己的知识还是有欠缺了,因此我决定近期重新翻一翻很早以前买的《c++ primer》 学习一下,并整理学习笔记背景介绍为什么会想到再次重新学习c++的基础内容呢?目前来看我所掌握的并不是最新的c++标准,而是“c with class” 的内容,而且很明显最近在关注一些新的cpp库的时候,发现它的语法我很多地方都没见过,虽然可以根据它的写法来大致猜到它到底用了什么东西,或者说在实现什么功能,但是要自己写,可能无法写出这种语法。而且明显感觉到新的标准加入了很多现代编程语言才有的内容,比如正则表达式、lambda表达式等等。这些都让写c++变得容易,写出的代码更加易读,使其脱离了上古时期的烙印更像现代的编程语言,作为一名靠c++吃饭的程序员,这些东西必须得会的。看书、学编程总少不了写代码并编译运行它。这次我把我写代码的环境更换到了mac平台,在mac平台上使用 vim + g++的方式。这里要提一句,在mac 的shell中,g++和gcc默认使用的是4.8的版本,许多新的c++标准并不被支持,需要下载最新的编译器并使用替换环境中使用的默认编译器,使其更新到最新版本gcc / g++ 使用在shell环境中,不再像visual studio开发环境中那样,只要点击build就一键帮你编译链接生成可执行程序了。shell中所有一切都需要你使用命令行来搞定,好在gcc/g++的使用并不复杂,记住几个常用参数就能解决日常80%的使用场景了,下面罗列一些常用的命令-o 指定生成目标文件位置和名称-l 指定连接库文件名称,一般库以lib开头但是在指定名称时不用加lib前缀,例如要链接libmath.o 可以写成-lmath-L 指定库所在目录-Wall 打印所有警告,一般编译时打开这个-E 仅做预处理,不进行编译-c 仅编译,不进行链接-static 编译为静态库-share 编译为动态库-Dname=definition 预定义一个值为definition的,名称为name的宏-ggdb -level 生成调试信息,level可以为1 2 3 默认为2-g -level 生成操作系统本地格式的调试信息 -g相比于-ggdb 来说会生成额外的信息-O0/O1/O2/O3 尝试优化-Os 对生成的文件大小进行优化常用的编译命令一般是 g++ -Wall -o demo demo.cpp开启所有警告项,并编译demo.cpp 生成demo程序 ## 基本数据类型与变量 ### 算术类型 这里说的基本数据类型主要是算术类型,按占用内存空间从小到大排序 char、bool(这二者应该是相同的)、short、wchar_t、int、long、longlong、float、double、long double。当然它们有的还有有符号与无符号的区别,这里就不单独列出了 一般来说,我们脑袋中记住的它们的大小好像是固定,比如wchar_t 占2个字节,int占4个字节。单实际上c++ 并没有给这些类型的大小都定义死,而是固定了一个最小尺寸,而具体大小究竟定义为多少,不同的编译器有不同的实现,比如我尝试的wchar_t 类型在vc 编译环境中占2个字节,而g++编译出来的占4一个字节。下面的表是c++ 规定的部分类型所占内存空间大小 | 类型 | 含义 | 最小尺寸 | |:----------|:--------------|:-----| | bool | 布尔类型 | 未定义 | | char | 字符 | 8位 | | wchar_t | 宽字符 | 16位 | | char16_t | Unicode字符 | 16位 | | char32_t | Unicode字符 | 32位 | | short | 短整型 | 16位 | | int | 整型 | 32位 | | long | 长整型 | 32位 | | longlong | 长整型 | 64位 | | float | 单精度浮点数 | 32位 | | double | 双精度浮点数 | 64位 | 另外c++的标准还规定 一个int类型至少和一个short一样大,long至少和int一样大、一个longlong至少和一个long一样大。 ### 有符号数与无符号数 数字类型分为有符号和无符号的,默认上述都是有符号的,在这些类型中加入unsigned 表示无符号,而char分为 signed char、char、unsigned char 三种类型。但是实际使用是只能选有符号或者无符号的。根据编译器不同,char的表现不同。 一般在使用这些数据类型的时候有如下原则 1. 明确知晓数值不可能为负的情况下使用unsigned 类型 2. 使用int进行算数运行,如果数值超过的int的表示范围则使用 longlong类型 3. 算术表达式中不要使用char或者bool类型 4. 如果需要使用一个不大的整数,必须指定是signed char 还是unsigned char 5. 执行浮点数运算时使用double ### 类型转化 当在程序的某处我们使用了一种类型,而实际对象应该取另一种类型时,程序会自动进行类型转化,类型转化主要分为隐式类型转化和显示类型转化。 数值类型进行类型转化时,一般遵循如下规则: 1. 把数字类型转化为bool类型时,0值会转化为false,其他值最后会被转化为true 2. 当把bool转化为非bool类型时,false会转化为0,true会被转化为1 3. 把浮点数转化为整型时,仅保留小数点前面的部分 4. 把整型转化为浮点数时,小数部分为0;如果整数的大小超过浮点数表示的范围,可能会损失精度 5. 当给无符号类型的整数赋值一个超过它表示范围的数时,会发生溢出。实际值是赋值的数对最大表示数取余数的结果 6. 当给有符号的类型一个超出它表示范围的值时,具体结果会根据编译器的不同而不同 7. 有符号数与无符号数混用时,结果会自动转化为无符号数 (使用小转大的原则,尽量不丢失精度) **由于bool转化为数字类型时非0即1,注意不要在算术表达式中使用bool类型进行运算** 下面是类型转化的具体例子 ```cpp bool b = 42; // b = true int i = b; // i = 1 i = 3.14; // i = 3; double d = i; // d = 3.0 unsigned char c = -1; // c = 256 signed char c2 = c; // c2 = 0 gcc 中 255在内存中的表现形式为0xff,+1 变为0x00 并向高位溢出,所以结果为0 ``` 上述代码的最后一个语句发生了溢出,**对于像溢出这种情况下。不同的编译器有不同的处理方式,得到的结果可能不经相同,在编写代码时需要避免此类情况的出现** 尽管我们知道不给一个无符号数赋一个负数,但是经常会在不经意间犯下这样的错误,例如当一个算术表达式中既有无符号数,又有有符号数的时候。例如下面的代码 ```cpp unsigned u = 10; int i = -42; printf("%d\r\n", u + i); // -32 printf("%u\r\n", u + i); //4294967264 ``` 那么该如何计算最后的结果呢,这里直接根据它们的二进制值来进行计算,然后再转化为具体的10进制数值,例如u = 0x0000000A,i = 0xffffffd6;二者相加得到 0xffffffEO, 如果转化为int类型,最高位是1,为负数,其余各位取反然后加一得到0x20,最终的结果就是-32,而无符号,最后的值为4294967264 ### 字面值常量 一般明确写出来数值内容的称之为字面值常量,从汇编的角度来看,能直接写入代码段中数值。例如32、0xff、"hello world" 这样内容的数值 #### 整数和浮点数的字面值 整数的字面值可以使用二进制、8进制、10进制、16进制的方式给出。而浮点数一般习惯上以科学计数法的形式给出 1. 二进制以 0b开头,八进制以0开头,十六进制以0x开头 2. 数值类型的字面值常量最终会以二进制的形式写入变量所在内存,如何解释由变量的类型决定,默认10进制是带符号的数值,其他的则是不带符号的 3. 十进制的字面值类型是int、long、longlong中占用空间最小的(前提是类型能容纳对应的数值) 4. 八进制、十六进制的字面值类型是int、unsigned int、long、unsigned long、longlong和unsigned longlong 中尺寸最小的一个(同样的要求对应类型能容纳对应的数值) 5. 浮点数的字面值用小数或者科学计数法表示、指数部分用e或者E标示 #### 字符和字符串的字面值常量 由单引号括起来的一个字符是char类型的字面值,双引号括起来的0个或者多个字符则构成字符串字面值常量。字符串实际上是一个字符数组,数组中的每个元素存储对应的字符。**这个数组的大小等于字符串中字符个数加1,多出来一个用于存储结尾的\0** 有两种类型的字符程序员是不能直接使用的,一类是不可打印的字符,如回车、换行、退格等格式控制字符,另一类是c/c++语言中有特殊用途的字符,例如单引号表示字符、双引号表示一个字符串,在这些情况下需要使用转义字符. 1. 转义以\开头,后面只转义仅接着的一个字符 2. 转义可以以字符开始,也可以以数字开始,数字在最后会被转化为对应的ASCII字符 3. \x后面跟16进制数、\后面跟八进制数、八进制数只取后面的3个;十六进制数则只能取两个数值(最多表示一个字节) ```cpp '\\' // 表示一个\字符 "\"" //表示一个" "\155" //表示一个 155的8进制数,8进制的155转化为10进制为109 从acsii表中可以查到,109对应的是M "\x6D" ``` 一般来讲我们很难通过字面值常量知道它到底应该是具体的哪种类型,例如 15既可以表示short、int、long、也是是double等等类型。为了准确表达字面值常量的类型,我们可以加上特定的前缀或者后缀来修饰它们。常用的前缀和后缀如下表所示: | 前缀 | 含义 | |:------|:-----------------------| | L'' | 宽字节 | | u8"" | utf-8字符串 | | 42ULL | unsgined longlong | | f | 单精度浮点数 | | 3L | long类型 | | 3.14L | long double | | 3LL | longlong | | u'' | char16_t Unicode16字符 | | U'' | char32_t Unicode32字符 | ## 变量 变量为程序提供了有名的,可供程序操作的内存空间,变量都有具体的数据类型、所在内存的位置以及存储的具体值(即使是未初始化的变量,也有它的默认值)。变量的类型决定它所占内存的大小、如何解释对应内存中的值、以及它能参与的运算类型。在面向对象的语言中,变量和对象一般都可以替换使用 ### 变量的定义与初始化 变量的定义一般格式是类型说明符其后紧随着一个或者多个变量名组成的列表,多个变量名使用逗号隔开。最后以分号结尾。 一般在定义变量的同时赋值,叫做变量的初始化。而赋值语句结束之后,在其他地方使用赋值语句对其进行赋值,被称为赋值。从汇编的角度来看,变量的初始化是,在变量进入它的生命有效期时,对那块内存执行的内存拷贝操作。而赋值则需要分解为两条语句,一个寻址,一个值拷贝。 c++11之后支持初始化列表进行初始化,在使用初始化列表进行初始化时如果出现初始值存在精度丢失的情况时会报错 c++11之后的列表初始化语句,支持使用赋值运算幅、赋值运算符加上{}、或者直接使用{}、直接使用() ```cpp int i = 3.14; //正常 int i(3.14); //正常 int i{3.14}; //报错,使用初始化列表进行初始化时,由double到int可能会发生精度丢失 int i(3.14); //正常 ``` 如果变量在定义的时候未给定初始值,则会执行默认初始化操作,全局变量会被赋值为0,局部变量则是未初始化的状态;它的值是不确定的。这个所谓的默认初始化操作,其实并不是真的那个时候执行了什么初始化语句。全局变量被初始化为0,主要是因为,在程序加载之初,操作系统会将数据段的内存都初始化为0,而局部变量,则是在进入函数之后,初始化栈,具体初始化为何值,根据平台的不同而不同 ### 声明与定义的关系 为了允许把程序拆分为多个逻辑部分来编写,c++支持分离式编译机制,该机制允许将程序分割为若干个文件,每个文件可被独立编译。 如果将程序分为多个文件,则需要一种在文件中共享代码的方法。c++中这种方法是将声明与定义区分开来。在我之前的博客中,有对应的说明。声明只是告诉编译器这个符号可以使用,它是什么类型,占多少空间,但前对它执行的这种操作是否合法。最终会生成一个符号表,在链接的时候根据具体地址,再转化为具体的二进制代码。而定义则是真正为它分配内存空间,以至于后续可以通过一个具体的地址访问它。 声明只需要在定义语句的前面加上extern关键字。如果extern 关键字后面跟上了显式初始化语句,则认为该条语句是变量的定义语句。变量可以声明多次但是只能定义一次。另外在函数内部不允许初始化一个extern声明的变量 ```cpp int main() { extern int i = 0; //错误 return 0; } ``` 一个好的规范是声明都在放在对应的头文件中,在其他地方使用时引入该头文件,后续要修改,只用修改头文件的一个地方。一个坏的规范是,想用了,就在cpp文件中使用extern声明,这样会导致声明有多份,修改定义,其他声明都得改,项目大了,想要找起来就不那么容易了。 ### 变量作用域 变量的作用域始于声明语句,终结于声明语句所在作用域的末端 1. 局部变量在整个函数中有效 2. 普通全局变量在整个程序中都有效果 3. 花括号中定义的变量仅在这对花括号中有效 作用域可以存在覆盖,并且以最新的定义的覆盖之前的 ```cpp int i = 10; void func() { int i = 20; { string i = "hello world"; cout
2021年01月17日
0 阅读
0 评论
0 点赞
2019-04-14
VC+++ 操作word
最近完成了一个使用VC++ 操作word生成扫描报告的功能,在这里将过程记录下来,开发环境为visual studio 2008导入接口首先在创建的MFC项目中引入word相关组件右键点击 项目 --> 添加 --> 新类,在弹出的对话框中选择Typelib中的MFC类。然后在弹出的对话框中选择文件,从文件中导入MSWORD.OLB组件。这个文件的路径一般在C:\Program Files (x86)\Microsoft Office\Office14 中,注意:最后一层可能不一定是Office14,这个看机器中安装的office 版本。选择之后会要求我们决定导入那些接口,为了方便我们导入所有接口。导入之后可以看到项目中省成本了很多代码文件,这些就是系统生成的操作Word的相关类。这里编译可能会报错,error C2786: “BOOL (HDC,int,int,int,int)”: __uuidof 的操作数无效解决方法:修改对应头文件#import "C:\\Program Files\\Microsoft Office\\Office14\\MSWORD.OLB" no_namespace为:#import "C:\\Program Files\\Microsoft Office\\Office14\\MSWORD.OLB" no_namespace raw_interfaces_only \ rename("FindText","_FindText") \ rename("Rectangle","_Rectangle") \ rename("ExitWindows","_ExitWindows")再次编译,错误消失常见接口介绍要了解一些常见的类,我们首先需要明白这些接口的层次结构:Application(WORD 为例,只列出一部分) Documents(所有的文档) Document(一个文档) ...... Templates(所有模板) Template(一个模板) ...... Windows(所有窗口) Window Selection View Selection(编辑对象) Font Style Range这些组件其实是采用远程调用的方式调用word进程来完成相关操作。Application:相当于一个word进程,每次操作之前都需要一个application对象,这个对象用于创建一个word进程。Documents:相当于word中打开的所有文档,如果用过word编辑多个文件,那么这个概念应该很好理解Templates:是一个模板对象,至于word模板,不了解的请自行百度Windows:word进程中的窗口Selection:编辑对象。也就是我们要写入word文档中的内容。一般包括文本、样式、图形等等对象。回忆一下我们手动编写word的情景,其实使用这些接口是很简单的。我们在使用word编辑的时候首先会打开word程序,这里对应在代码里面就是创建一个Application对象。然后我们会用word程序打开一个文档或者新建一个文档。这里对应着创建Documents对象并从中引用一个Document对象表示一个具体的文档。当然这个Document对象可以是新建的也可以是打开一个现有的。接着就是进行相关操作了,比如插入图片、插入表格、编写段落文本等等了。这些都对应着创建类似于Font、Style、TypeText对象,然后将这些对象进行添加的操作了。说了这么多。这些接口这么多,我怎么知道哪个接口对应哪个对象呢,而且这些参数怎么传递呢?其实这个问题很好解决。我们可以手工进行相关操作,然后用宏记录下来,最后我们再将宏中的VB代码转化为VC代码即可。相关操作为了方便在项目中使用,这里创建一个类用于封装Word的相关操作class CCreateWordReport { private: CApplication m_wdApp; CDocuments m_wdDocs; CDocument0 m_wdDoc; CSelection m_wdSel; CRange m_wdRange; CnlineShapes m_wdInlineShapes; CnlineShape m_wdInlineShape; public: CCreateWordReport(); virtual ~CCreateWordReport(); public: //操作 //**********************创建新文档******************************************* BOOL CreateApp(); //创建一个新的WORD应用程序 BOOL CreateDocuments(); //创建一个新的Word文档集合 BOOL CreateDocument(); //创建一个新的Word文档 BOOL Create(); //创建新的WORD应用程序并创建一个新的文档 void ShowApp(); //显示WORD文档 void HideApp(); //隐藏word文档 //**********************打开文档********************************************* BOOL OpenDocument(CString fileName);//打开已经存在的文档。 BOOL Open(CString fileName); //创建新的WORD应用程序并打开一个已经存在的文档。 BOOL SetActiveDocument(short i); //设置当前激活的文档。 //**********************保存文档********************************************* BOOL SaveDocument(); //文档是以打开形式,保存。 BOOL SaveDocumentAs(CString fileName);//文档以创建形式,保存。 BOOL CloseDocument(); void CloseApp(); //**********************文本书写操作***************************************** void WriteText(CString szText); //当前光标处写文本 void WriteNewLineText(CString szText, int nLineCount = 1); //换N行写字 void WriteEndLine(CString szText); //文档结尾处写文本 void WholeStory(); //全选文档内容 void Copy(); //复制文本内容到剪贴板 void InsertFile(CString fileName); //将本地的文件全部内容写入到当前文档的光标处。 void InsertTable(int nRow, int nColumn, CTable0& wdTable); //**********************图片插入操作***************************************** void InsertShapes(CString fileName);//在当前光标的位置插入图片 //**********************超链接插入操作***************************************** void InsertHyperlink(CString fileLink);//超级链接地址,可以是相对路径。 //***********************表格操作表格操作********************************** BOOL InsertTableToMarkBook(const CString csMarkName, int nRow, int nColumn, CTable0& wdTable); //表格行数与列数 BOOL WriteDataToTable(CTable0& wdTable, int nRow, int nColumn, const CString &csData); //往表格中写入输入 };BOOL CCreateWordReport::CreateApp() { if (FALSE == m_wdApp.CreateDispatch("word.application")) { AfxMessageBox("Application创建失败,请确保安装了word 2000或以上版本!", MB_OK|MB_ICONWARNING); return FALSE; } return TRUE; } BOOL CCreateWordReport::CreateDocuments() { if (FALSE == CreateApp()) { return FALSE; } m_wdDocs = m_wdApp.get_Documents(); if (!m_wdDocs.m_lpDispatch) { AfxMessageBox("Documents创建失败!", MB_OK|MB_ICONWARNING); return FALSE; } return TRUE; } BOOL CCreateWordReport::CreateDocument() { if (!m_wdDocs.m_lpDispatch) { AfxMessageBox("Documents为空!", MB_OK|MB_ICONWARNING); return FALSE; } COleVariant varTrue(short(1),VT_BOOL),vOptional((long)DISP_E_PARAMNOTFOUND, VT_ERROR); CComVariant Template(_T("")); //没有使用WORD的文档模板 CComVariant NewTemplate(false),DocumentType(0),Visible; m_wdDocs.Add(&Template,&NewTemplate,&DocumentType,&Visible); //得到document变量 m_wdDoc = m_wdApp.get_ActiveDocument(); if (!m_wdDoc.m_lpDispatch) { AfxMessageBox("Document获取失败!", MB_OK|MB_ICONWARNING); return FALSE; } //得到selection变量 m_wdSel = m_wdApp.get_Selection(); if (!m_wdSel.m_lpDispatch) { AfxMessageBox("Select获取失败!", MB_OK|MB_ICONWARNING); return FALSE; } //得到Range变量 m_wdRange = m_wdDoc.Range(vOptional,vOptional); if(!m_wdRange.m_lpDispatch) { AfxMessageBox("Range获取失败!", MB_OK|MB_ICONWARNING); return FALSE; } return TRUE; } BOOL CCreateWordReport::Create() { if (FALSE == CreateDocuments()) { return FALSE; } return CreateDocument(); } BOOL CCreateWordReport::OpenDocument(CString fileName) { if (!m_wdDocs.m_lpDispatch) { AfxMessageBox("Documents为空!", MB_OK|MB_ICONWARNING); return FALSE; } COleVariant vTrue((short)TRUE), vFalse((short)FALSE), vOptional((long)DISP_E_PARAMNOTFOUND, VT_ERROR), vZ((short)0); COleVariant vFileName(_T(fileName)); //得到document变量 m_wdDoc = m_wdDocs.Open( vFileName, // FileName vTrue, // Confirm Conversion. vFalse, // ReadOnly. vFalse, // AddToRecentFiles. vOptional, // PasswordDocument. vOptional, // PasswordTemplate. vOptional, // Revert. vOptional, // WritePasswordDocument. vOptional, // WritePasswordTemplate. vOptional, // Format. // Last argument for Word 97 vOptional, // Encoding // New for Word 2000/2002 vOptional, // Visible /*如下4个是word2003需要的参数。本版本是word2000。*/ vOptional, // OpenAndRepair vZ, // DocumentDirection wdDocumentDirection LeftToRight vOptional, // NoEncodingDialog vOptional ); if (!m_wdDoc.m_lpDispatch) { AfxMessageBox("Document获取失败!", MB_OK|MB_ICONWARNING); return FALSE; } //得到selection变量 m_wdSel = m_wdApp.get_Selection(); if (!m_wdSel.m_lpDispatch) { AfxMessageBox("Select获取失败!", MB_OK|MB_ICONWARNING); return FALSE; } //得到全部DOC的Range变量 m_wdRange = m_wdDoc.Range(vOptional,vOptional); if(!m_wdRange.m_lpDispatch) { AfxMessageBox("Range获取失败!", MB_OK|MB_ICONWARNING); return FALSE; } return TRUE; } BOOL CCreateWordReport::Open(CString fileName) { if (FALSE == CreateDocuments()) { return FALSE; } return OpenDocument(fileName); } BOOL CCreateWordReport::SetActiveDocument(short i) { COleVariant vIndex(_T(i)),vOptional((long)DISP_E_PARAMNOTFOUND, VT_ERROR); m_wdDoc.AttachDispatch(m_wdDocs.Item(vIndex)); m_wdDoc.Activate(); if (!m_wdDoc.m_lpDispatch) { AfxMessageBox("Document获取失败!", MB_OK|MB_ICONWARNING); return FALSE; } //得到selection变量 m_wdSel = m_wdApp.get_Selection(); if (!m_wdSel.m_lpDispatch) { AfxMessageBox("Select获取失败!", MB_OK|MB_ICONWARNING); return FALSE; } //得到全部DOC的Range变量 m_wdRange = m_wdDoc.Range(vOptional,vOptional); if(!m_wdRange.m_lpDispatch) { AfxMessageBox("Range获取失败!", MB_OK|MB_ICONWARNING); return FALSE; } // HideApp(); return TRUE; } BOOL CCreateWordReport::SaveDocument() { if (!m_wdDoc.m_lpDispatch) { AfxMessageBox("Document获取失败!", MB_OK|MB_ICONWARNING); return FALSE; } m_wdDoc.Save(); return TRUE; } BOOL CCreateWordReport::SaveDocumentAs(CString fileName) { if (!m_wdDoc.m_lpDispatch) { AfxMessageBox("Document获取失败!", MB_OK|MB_ICONWARNING); return FALSE; } COleVariant covOptional((long)DISP_E_PARAMNOTFOUND,VT_ERROR); COleVariant varZero((short)0); COleVariant varTrue(short(1),VT_BOOL); COleVariant varFalse(short(0),VT_BOOL); COleVariant vFileName(_T(fileName)); m_wdDoc.SaveAs( vFileName, covOptional, covOptional, covOptional, covOptional, covOptional, covOptional, covOptional, covOptional, covOptional, covOptional, covOptional, covOptional, covOptional, covOptional, covOptional ); return TRUE; } BOOL CCreateWordReport::CloseDocument() { COleVariant vTrue((short)TRUE), vFalse((short)FALSE), vOptional((long)DISP_E_PARAMNOTFOUND, VT_ERROR); m_wdDoc.Close(vFalse, // SaveChanges. vTrue, // OriginalFormat. vFalse // RouteDocument. ); m_wdDoc.AttachDispatch(m_wdApp.get_ActiveDocument()); if (!m_wdDoc.m_lpDispatch) { AfxMessageBox("Document获取失败!", MB_OK|MB_ICONWARNING); return FALSE; } //得到selection变量 m_wdSel = m_wdApp.get_Selection(); if (!m_wdSel.m_lpDispatch) { AfxMessageBox("Select获取失败!", MB_OK|MB_ICONWARNING); return FALSE; } //得到全部DOC的Range变量 m_wdRange = m_wdDoc.Range(vOptional,vOptional); if(!m_wdRange.m_lpDispatch) { AfxMessageBox("Range获取失败!", MB_OK|MB_ICONWARNING); return FALSE; } return TRUE; } void CCreateWordReport::CloseApp() { COleVariant vTrue((short)TRUE), vFalse((short)FALSE), vOptional((long)DISP_E_PARAMNOTFOUND, VT_ERROR); m_wdDoc.Save(); m_wdApp.Quit(vFalse, // SaveChanges. vTrue, // OriginalFormat. vFalse // RouteDocument. ); //释放内存申请资源 m_wdInlineShape.ReleaseDispatch(); m_wdInlineShapes.ReleaseDispatch(); //m_wdTb.ReleaseDispatch(); m_wdRange.ReleaseDispatch(); m_wdSel.ReleaseDispatch(); //m_wdFt.ReleaseDispatch(); m_wdDoc.ReleaseDispatch(); m_wdDocs.ReleaseDispatch(); m_wdApp.ReleaseDispatch(); } void CCreateWordReport::WriteText(CString szText) { m_wdSel.TypeText(szText); } void CCreateWordReport::WriteNewLineText(CString szText, int nLineCount /* = 1 */) { int i; if (nLineCount <= 0) { nLineCount = 0; } for (i = 0; i < nLineCount; i++) { m_wdSel.TypeParagraph(); } WriteText(szText); } void CCreateWordReport::WriteEndLine(CString szText) { m_wdRange.InsertAfter(szText); } void CCreateWordReport::WholeStory() { m_wdRange.WholeStory(); } void CCreateWordReport::Copy() { m_wdRange.CopyAsPicture(); } void CCreateWordReport::InsertFile(CString fileName) { COleVariant vFileName(fileName), vTrue((short)TRUE), vFalse((short)FALSE), vOptional((long)DISP_E_PARAMNOTFOUND, VT_ERROR), vNull(_T("")); /* void InsertFile(LPCTSTR FileName, VARIANT* Range, VARIANT* ConfirmConversions, VARIANT* Link, VARIANT* Attachment); */ m_wdSel.InsertFile( fileName, vNull, vFalse, vFalse, vFalse ); } void CCreateWordReport::InsertShapes(CString fileName) { COleVariant vTrue((short)TRUE), vFalse((short)FALSE), vOptional((long)DISP_E_PARAMNOTFOUND, VT_ERROR); m_wdInlineShapes=m_wdSel.get_InlineShapes(); m_wdInlineShape=m_wdInlineShapes.AddPicture(fileName,vFalse,vTrue,vOptional); } void CCreateWordReport::InsertHyperlink(CString fileLink) { COleVariant vAddress(_T(fileLink)),vSubAddress(_T("")); CRange aRange = m_wdSel.get_Range(); CHyperlinks vHyperlinks(aRange.get_Hyperlinks()); vHyperlinks.Add( aRange, //Object,必需。转换为超链接的文本或图形。 vAddress, //Variant 类型,可选。指定的链接的地址。此地址可以是电子邮件地址、Internet 地址或文件名。请注意,Microsoft Word 不检查该地址的正确性。 vSubAddress, //Variant 类型,可选。目标文件内的位置名,如书签、已命名的区域或幻灯片编号。 vAddress, //Variant 类型,可选。当鼠标指针放在指定的超链接上时显示的可用作“屏幕提示”的文本。默认值为 Address。 vAddress, //Variant 类型,可选。指定的超链接的显示文本。此参数的值将取代由 Anchor 指定的文本或图形。 vSubAddress //Variant 类型,可选。要在其中打开指定的超链接的框架或窗口的名字。 ); aRange.ReleaseDispatch(); vHyperlinks.ReleaseDispatch(); }这样我们就封装好了一些基本的操作,其实这些操作都是我自己根据网上的资料以及VB宏转化而来得到的代码。特殊操作在这里主要介绍一些比较骚的操作,这也是这篇文章主要有用的内容,前面基本操作网上都有源代码直接拿来用就OK了,这里的骚操作是我在项目中使用的主要操作,应该有应用价值。先请各位仔细想想,如果我们要根据前面的代码,从0开始完全用代码生成一个完整的报表是不是很累,而且一般报表都会包含一些通用的废话,这些话基本不会变化。如果将这些写到代码里面,如果后面这些话变了,我们就要修改并重新编译,是不是很麻烦。所以这里介绍的第一个操作就是利用模板和书签在合适的位置插入内容。书签的使用首先我们在Word中的适当位置创建一个标签,至于如何创建标签,请自行百度。然后在代码中的思路就是在文档中查找我们的标签,再获取光标的位置,最后就是在该位置处添加相应的内容了,这里我们举一个在光标位置插入文本的例子:void CCreateWordReport::WriteTextToBookMark(const CString& csMarkName, const CString& szText) { CBookmarks bks = m_wdDoc.get_Bookmarks(); //获取文档中的所有书签 CBookmark0 bk; COleVariant bk_name(csMarkName); bk = bks.Item(&bk_name); //查询对应名称的书签 CRange hRange = bk.get_Range(); //获取书签位置 if (hRange != NULL) { hRange.put_Text(szText); //在该位置处插入文本 } //最后不要忘记清理相关资源 hRange.ReleaseDispatch(); bk.ReleaseDispatch(); bks.ReleaseDispatch(); }表格的使用在word报表中表格应该是一个重头戏,表格中常用的接口如下:CTables0: 表格集合CTable0: 某个具体的表格,一般通过CTables来创建CTableCColumn: 表格列对象CRow:表格行对象CCel:表格单元格对象创建表格一般的操作如下:void CCreateWordReport::InsertTable(int nRow, int nColumn, CTable0& wdTable) { VARIANT vtDefault; COleVariant vtAuto; vtDefault.vt = VT_INT; vtDefault.intVal = 1; vtAuto.vt = VT_INT; vtAuto.intVal = 0; CTables0 wordtables = m_wdDoc.get_Tables(); wdTable = wordtables.Add(m_wdSel.get_Range(), nRow, nColumn, &vtDefault, &vtAuto); wordtables.ReleaseDispatch(); }往表格中写入内容的操作如下:BOOL CCreateWordReport::WriteDataToTable(CTable0& wdTable, int nRow, int nColumn, const CString &csData) { CCell cell = wdTable.Cell(nRow, nColumn); cell.Select(); //将光标移动到单元格 m_wdSel.TypeText(csData); cell.ReleaseDispatch(); return TRUE; }合并单元格的操作如下:CTable0 wdTable; InsertTable(5, 3, wdTable); //创建一个5行3列的表格 CCell cell = wdTable.Cell(1, 1); //获得第一行第一列的单元格 //设置第二列列宽 CColumns0 columns = wdTable.get_Columns(); CColumn col; col.AttachDispatch(columns.Item(2)); col.SetWidth(40, 1); cell.Merge(wdTable.Cell(5, 1)); //合并单元格,一直合并到第5行的第1列。 cell.SetWidth(30, 1); cell.ReleaseDispatch();合并单元格用的是Merge函数,该函数的参数是一个单元格对象,表示合并结束的单元格。这里合并类似于我们画矩形时提供的左上角坐标和右下角坐标移动光标跳出表格当时由于需要连续的生成多个表格,当时我将前一个表格的数据填完,光标位于最后一个单元格里面,这个时候如果再插入的时候会在这个单元格里面插入表格,这个时候需要我手动向下移动光标,让光标移除到表格外。移动光标的代码如下:m_wdSel.MoveDown(&COleVariant((short)wdLine), &COleVariant((short)1), &COleVariant((short)wdNULL)); 这里wdLine 是word相关接口定义的,表示希望以何种单位来移动,这里我是以行为单位。后面的1表示移动1行。但是我发现在面临换页的时候一次移动根本移动不出来,这个时候我又添加了一行这样的代码移动两行。但是问题又出现了,这一系列表格后面跟着另一个大标题,多移动几次之后可能会造成它移动到大标题的位置,而破坏我原来定义的模板,这个时候该怎么办呢?我采取的办法是,判断当前光标是否在表格中,如果是则移动一行,知道出了表格。这里的代码如下://移动光标,直到跳出表格外 while (TRUE) { m_wdSel.MoveDown(&COleVariant((short)wdLine), &COleVariant((short)1), &COleVariant((short)wdNULL)); m_wdSel.Collapse(&COleVariant((short)wdCollapseStart)); if (!m_wdSel.get_Information((long)wdWithInTable).boolVal) { break; } }样式的使用在使用样式的时候当然也可以用代码来定义,但是我们可以采取另一种方式,我们可以事先在模板文件中创建一系列样式,然后在需要的时候直接定义段落或者文本的样式即可m_wdSel.put_Style(COleVariant("二级标题")); //在当前光标处的样式定义为二级标题样式,这里的二级标题样式是我们在word中事先定义好的 m_wdSel.TypeText(csTitle); //在当前位置输出文本 m_wdSel.TypeParagraph(); //插入段落,这里主要为了换行,这个时候光标也会跟着动 m_wdSel.put_Style(COleVariant("正文")); //定义此处样式为正文样式 m_wdSel.TypeText(csText;插入图表我自己尝试用word生成的图表样式还可以,但是用代码插入的时候,样式就特别丑,这里没有办法,我采用GDI+绘制了一个饼图,然后将图片插入word中。BOOL CCreateWordReport::DrawVulInforPic(const CString& csMarkName, int nVulCnt, int nVulCris, int nHigh, int nMid, int nLow, int nPossible) { CBookmarks bks = m_wdDoc.get_Bookmarks(); COleVariant bk_name(csMarkName); CBookmark0 bk = bks.Item(&bk_name); bk.Select(); CnlineShapes isps = m_wdSel.get_InlineShapes(); COleVariant vFalse((short)FALSE); COleVariant vNull(""); COleVariant vOptional((long)DISP_E_PARAMNOTFOUND,VT_ERROR); //创建一个与桌面环境兼容的内存DC HWND hWnd = GetDesktopWindow(); HDC hDc = GetDC(hWnd); HDC hMemDc = CreateCompatibleDC(hDc); HBITMAP hMemBmp = CreateCompatibleBitmap(hDc, PICTURE_WIDTH + GLOBAL_MARGIN, PICTURE_LENGTH + 2 * GLOBAL_MARGIN + LENGED_BORDER_LENGTH); SelectObject(hMemDc, hMemBmp); //绘制并保存图表 DrawPie(hMemDc, nVulCnt, nVulCris, nHigh, nMid, nLow, nPossible); COleVariant vTrue((short)TRUE); CnlineShape isp=isps.AddPicture("D:\\Program\\WordReport\\WordReport\\test.png",vFalse,vTrue,vOptional); //以图片的方式插入图表 //设置图片的大小 isp.put_Height(141); isp.put_Width(423); bks.ReleaseDispatch(); bk.ReleaseDispatch(); isps.ReleaseDispatch(); isp.ReleaseDispatch(); DeleteObject(hMemDc); DeleteDC(hMemDc); ReleaseDC(hWnd, hDc); return TRUE; }最后,各个接口的参数可以参考下面的链接:.net Word office组件接口文档
2019年04月14日
2 阅读
0 评论
0 点赞
2019-04-06
算法与数据结构(七):快速排序
在上一篇中,回顾了一下针对选择排序的优化算法——堆排序。堆排序的时间复杂度为O(nlogn),而快速排序的时间复杂度也是O(nlogn)。但是快速排序在同为O(n*logn)的排序算法中,效率也是相对较高的,而且快速排序使用了算法中一个十分经典的思想——分治法;因此掌握快速排序还是很有必要的。快速排序的基本思想如下:在一组无序元素中,找到一个数作为基准数。将大于它的数全部移动到它的右侧,小于它的全部移动到右侧。在分成的两个区中,再次重复1到2 的步骤,直到所有的数全部有序下面还是来看一个例子[3,6,1,2,8,4,7]首先选取一个基准数,一般选择序列最左侧的数为基准数,也就是3,将小于3的数移动到3的左边,大于3的移动到3的右边,得到如下的序列[2,1,3,6,8,4,7]接着针对左侧的[2, 1] 这个序列和 [6, 8, ,4, 7]这两个序列再次执行这种操作,直到所有的数都变为有序为止。知道了具体的思路下面就是写算法了。void QSort(int a[], int n) { int nIdx = adjust(a, 0, n -1); //针对调整之后的数据左右两侧序列都再次进行调整 if(nIdx != -1) { QSort(&a[0], nIdx); QSort(&a[nIdx + 1], n - nIdx - 1); } }这里定义了一个函数作为快速排序的函数,函数需要传入序列的首地址以及序列中间元素的长度。在排序函数中只需要关注如何进行调整即可。这里进行了一个判断,当调整函数返回-1时表示不需要调整,也就是说此时已经都是有序的了,这个时候就不需要调整了。程序的基本框架已经完成了,剩下的就是如何编写调整函数了。调整的算法如下:首先定义两个指针,指向最右侧和最左侧,最左侧指针指向基准数所在位置先从右往左扫描,当发现右侧数小于基准值时,将基准值位置的数替换为该数,并且立刻从左往右扫描,直到找到一个数大于基准值,再次进行替换接着再次从右往左扫描,直到找到小于基准数的值;并再次改变扫描顺序,直到调整完毕最后直到两个指针重合,此时重合的位置就是基准值所在位置根据这个思路,可以编写如下代码int QuickSort(int a[], int nLow, int nHigh) { if (nLow >= nHigh) { return -1; } int tmp = a[nLow]; int i = nLow; int j = nHigh; while (i != j) { //先从右往左扫描,只到找到比基准值小的数 //将该数放到基准值的左侧 while (a[j] > tmp && j > i) { j--; } if (a[j] < tmp) { a[i]= a[j]; i++; } //接着从左往右扫描,直到找到比基准值大的数 //将该数放入到基准值的右侧 while (a[i] < tmp && i < j) { i++; } if (a[i] > tmp) { a[j] = a[i]; j--; } } a[i] = tmp; return i; }到此已经完成了快速排序的算法编写了。在有大量的数据需要进行排序时快速排序的效果比较好,如果数据量小,或者排序的序列已经是一个逆序的有序序列,它退化成O(n^2)。快速排序是一个不稳定的排序算法。
2019年04月06日
4 阅读
0 评论
0 点赞
1
2
3
...
15