C++学习笔记(一)

前言

记录一些自己感觉比较有用的概念. 实际上就是一份从入门到放弃的笔记,毕竟还是喜欢从网上逛博客学知识,零散些没什么,能够将这些知识串起来就很有用了。

容器作为函数参数如何传参

1, void 函数名( vector< int> obj );

2, void 函数名( vector< int>* pobj );

3, void 函数名( const vector< int>* pobj ); // 在函数内不能改变 pobj 指向的对象 ,//调用时不会调用拷贝构造函数

4, void 函数名( vector< int>& obj );

5, void 函数名( const vector< int>& obj ); // 在函数内不能改变 obj 对象,// 在函数调用时调用了vector的拷贝构造函数

表达式放在构析函数中, 如果类的析构函数存在, 那么在每个类的生命期结束时它会被调用.析构函数由类名前面加一个波浪号来标识.

resize AND reserve

  1. capacity:指容器在分配新的存储空间之前能存储的元素总数.size:指当前容器所存储的元素个数.

  2. reserve表示容器预留空间,但并不是真正的创建对象,需要通过insert()或push_back()等创建对象.resize既分配了空间,也创建了对象.

  3. reserve只修改capacity大小,不修改size大小,resize既修改capacity大小,也修改size大小.

动态内存分配和指针

静态与动态内存分配的两个主要区别是:

1, 静态对象是有名字的变量, 直接对其进行操作,而动态对象是没有名字的变量,通过指针间接地对它进行操作;

2, 静态对象的分配与释放由编译器自动处理事情, 相反,动态对象的分配与释放,必须由程序员显式地管理但不需要做任何事情,相对来说比较容易出错它通过new和delete两个表达式来完成, 对于动态分配的内存惟一的访问方式是通过指针间接地访问

3, 没有办法给动态分配的数组的每个元素显式地指定一个初始值, 当用完了动态分配的对象或对象的数组时, 必须显式地释放这些内存, 可以通过使用 delete 表达式的两个版本之一来完成这件事情,而释放之后的内存则可以被程序重新使用.

4, 如果忘了删除动态分配的内存程序就会在结束时出现内存泄漏 (memory leak) 的问题

5, 由于 C++不允许成员函数与数据成员共享同一个名字, 所以在这样的情况下,一般的习惯是在数据成员名字前面加一个下划线.

6, 在类定义中被定义的成员函数会被自动当作是内联函数,此外我们也可以用inline关键字显式地要求一个函数被视为内联函数

7, 被声明为 static 的数据成员是一类特殊的共享数据成员无论这个类的对象被定义了多少个静态数据成员在程序中也只有一份.

.hpp与.h区别

.hpp,本质就是将.cpp的实现代码混入.h头文件当中,定义与实现都包含在同一文件,则该类的调用者只需要include该.hpp文件即可,无需再将cpp加入到project中进行编译.而实现代码将直接编译到调用者的obj文件中,不再生成单独的obj,采用hpp将大幅度减少调用project中的cpp文件数与编译次数,也不用再发布lib与dll文件,因此非常适合用来编写公用的开源库.

由于.hpp本质上是作为.h被调用者include的,所以当hpp文件中存在全局对象或者全局函数,而该hpp被多个调用者include时,将在链接时导致符号重定义错误.要避免这种情况,需要去除全局对象,将全局函数封装为类的静态方法.

Note: 函数返回运算结果的前提有3个:

1) 使用指针变量作为函数形参。

2)用接受运算结果的变量的指针(或地址)作为实参调用函数

3)函数中通过指针间接引用修改这些变量。

Note: 函数名既代表函数,又是函数的指针。 2) delete只能删除由new创建的动态对象,否则将导致程序错误。

类与对象

1, 类成员可以是数据, 函数或类型别名。所有成员必须在类的内部声明, 一旦类定义完成后, 就没有任何其他方式再增加成员了。

2, 类定义一般放在程序文件开头, 或者放到头文件中被程序文件包含, 此时这个定义是全局的。在全局作用域内, 该定义处处可见, 因此同一作用域内的所有函数都可以使用它。

成员访问控制

对类的成员进行访问,来自两个访问源: 类成员和类用户。类成员指的是类本身的成员函数,类用户指的是类外部的使用者,包括全局函数,另一个类的成员函数等。 无论数据成员还是函数成员,类的每个成员都有访问控制属性,由一下三个访问标号说明: public, private, protected. 类成员和类用户都可以访问公有成员,任何一个来自类外部的类用户都必须通过公有成员来访问。显然,public实现了类的外部接口。

只有类成员可以访问私有成员,类用户的访问是不允许的。显然,private实现了私有成员的隐蔽。

保护成员用protected标号说明,在不考虑继承的情况下,protected的性质和private的性质一致,但保护成员可以被派生类的类成员访问。总的来说可以参考如下表中所示

访问   public   protected   private
同一个类   yes   yes   yes
派生类   yes   yes   no
外部的类   yes   no   no
友元类   yes   yes   yes

另外需要说明:

1) 数据成员一般声明为 private,以实现信息的隐蔽.

2) 成员函数一般声明为 public,以提供外界使用的接口.

3) 构造函数一般声明为 public,以便创建类的对象.

4) 创建一个类类型的对象时,编译器会自动使用一个构造函数来初始化该对象,构造函数是一个特殊的、与类同名的成员函数,用于初始化每个数据成员来设置初始值。

5) 构造函数一般使用一个构造函数初始化列表,来初始化对象的数据成员.

6) 在类内部,声明成员函数是必需的,而定义成员函数则可选,可以在类外定义。在类内部定义的函数默认为 inline 函数.

示例

可用下面代码辅助理解

class MyClass {
public:
// 公有接口
void PublicFunction() {
// 可以在类外部调用
}

protected:
// 保护接口
void ProtectedFunction() {
// 在派生类中可以调用,但在类外部无法直接访问
}

private:
// 私有接口
void PrivateFunction() {
// 只能在类的内部调用,类外部无法直接访问
}
};

注意事项

下面需要注意的源自于林锐的《高质量C++/C编程指南》

1, 在多重循环中,如果有可能,应当将最长的循环放在最内层,最短的循环放在最外层,来提高程序运行效率。

2, 如果循环体内存在逻辑判断,并且循环次数很大,宜将逻辑判断移到循环体的外面.

3, Switch 是多分支选择语句,而 if 语句只有两个分支可供选择。虽然可以用嵌套的 if 语句来实现多分支选择,但那样的程序冗长难读。这是 switch 语句存在的理由。

常见的内存错误及其对策如下

1, 内存分配未成功,却使用了它。

2, 内存分配虽然成功,但是尚未初始化就引用它。

3, 内存分配成功并且已经初始化,但操作越过了内存的边界

4, 忘记了释放内存,造成内存泄露。

5, 释放了内存却继续使用它。

Note:

1) 用 malloc 或 new 申请内存之后,应该立即检査指针值是否为 NULL。防止使用指针值为 NULL 的内存

2) 不要忘记为数组和动态内存赋初值。防止将未被初始化的内存作为右值使用。

3) 避免数组或指针的下标越界,特别要当心发生“多 1”或者“少 1”操作。

4) 动态内存的申请与释放必须配对,防止内存泄漏。

5)用 free 或 delete 释放了内存之后,立即将指针设置为 NULL,防止产生“野指针”.

malloc 返回值的类型是 void *,所以在调用 malloc 时要显式地进行类型转换,将 void *转换成所需要的指针类型。

关于类的注意事项

类的基本思想是数据抽象和封装。数据抽象是种依赖于接口(interface)和实现(implementation)分离的编程(以及设计)技术。类的接口包括用户所能执行的操作;类的实现则包括类的数据成员、负责接口实现的函数体以及定义类所需的各种私有函数。

除了静态 static 数据成员外,数据成员不能在类体中被显式地初始化。

读取以及处理数据

1) file.good()是在文件读取或者写的过程中出现错误;或者读到文件最后继续读才会返回false;

2) 在读取数组文件的时候,用上了getline这个函数,其函数用法如下,其中默认就是按行读取,这样的好处就是,去模拟信号的输入。

// (buffer, stream_size, delimiter)
istream& getline(char*, int size, char='\n')

结合解引用和成员访问操作

解引用迭代器可获得迭代器所指的对象,如果该对象的类型恰好是类,就有可能希望进一步访问它的成员。例如,对于一个由字符串组成的 vector 对象来说,要想检査其元素是否为空,令 it 是该 vector 对象的迭代器,只需检査主 it 所指字符串是否为空就可以了,其代码如下所示

(*it).empty()

注意,(*it).empty()中的圆括号必不可少,

为了简化上述表达式,C++语言定义了箭头运算符(->)。箭头运算符把解引用和成员访问两个操作结合在一起,也就是说,it->mem 和(*it).mem 表达的意思相同。

Note: String 和 vector 是两种最重要的标准库类型。string 对象是一个可变长的字符序列,vector 对象是一组同类型对象的容器。 迭代器允许对容器中的对象进行间接访问,对于 string 对象和 vector 对象来说,可以通过迭代器访问元素或者在元素间移动。 数组和指向数组元素的指针在一个较低的层次上实现了与标准库类型 string 和 vector 类似的功能。一般来说,应该优先选用标准库提供的类型,之后再考虑 C++语言内置的低层的替代品数组或指针

Note: ++运算符(++operator)是迭代器和指针定义的递增运算符。执行“加 1”操作使得迭代器指向下一个元素。

表达式

当一个对象被用作右值的时候,用的是对象的值(内容);当对象被用作左值的时候,用的是对象的身份(在内存中的位置)。 后置递增运算符的优先级高于解引用运算符,因此*pbeg++等价于*(pbeg++)。pbeg++把 pbeg 的值加 1, 然后返回 pbeg 的初始值的副本作为其求值结果,此时解引用运算符的运算对象是 pbeg 未增加之前的值。最终,这条语句输出 pbeg 开始时指向的那个元素,并将指针向前移动一个位置。 对于逗号运算符来说,首先对左侧的表达式求值,然后将求值结果丢弃掉。逗号运算符真正的结果是右侧表达式的值。如果右侧运算对象是左值,那么最终的求值结果也是左值。

函数相关

通过调用运算符(call operator)来执行函数。调用运算符的形式是一对圆括号,它作用于ー个表达式,该表达式是函数或者指向函数的指针:圆括号之内是一个用逗号隔开的实参(argument)列表,我们用实参初始化函数的形参。调用表达式的类型就是函数的返回类型。

函数的形参列表可以为空,但是不能省略。要想定义一个不带形参的函数,最常用的办法是书写一个空的形参列表。不过为了与 C 语言兼容,也可以使用关键字 void 表示函数没有形参. 形参列表中的形参通常用逗号隔开,其中每个形参都是含有一个声明符的声明。即使两个形参的类型一样,也必须把两个类型都写出来.

大多数类型都能用作函数的返回类型。一种特殊的返回类型是 void,它表示函数不返回任何值。函数的返回类型不能是数组,类型或函数类型,但可以是指向数组或函数的指针。

和其他变量一样,形参的类型决定了形参和实参交互的方式。如果形参是引用类型,它将绑定到对应的实参上;否则,将实参的值拷贝后赋给形参。 当形参是引用类型时,我们说它对应的实参被引用传递或者函数被传引用调用。和其他引用一样,引用形参也是它绑定的对象的别名:也就是说,引用形参是它对应的实参的别名 当实参的值被拷贝给形参时,形参和实参是两个相互独立的对象。我们说这样的实参被值传递(passed by value)或者函数被传值调用(called by value)。

指针就是一个存放地址的变量, 指针的行为和其他非引用类型一样。当执行指针拷贝操作时,拷贝的是指针的值。拷贝之后,两个指针是不同的指针。因为指针使我们可以间接地访问它所指的对象,所以通过指针可以修改它所指对象的值.

拷贝大的类类型对象或者容器对象比较低效,甚至有的类类型(包括 IO 类型在内)根本就不支持拷贝操作。当某种类型不支持拷贝操作时,函数只能通过引用形参访问该类型的对象。

举个例子,我们准备编写一个函数比较两个 string 对象的长度。因为 string 对象可能会非常长,所以应该尽量避免直接拷贝它们,这时使用引用形参是比较明智的选择又因为比较长度无须改变 string 对象的内容,所以把形参定义成对常量的引用.

Note: 如果函数无须改变引用形参的值,最好将其声明为常量引用。

一个函数只能返回一个值,然而有时函数需要同时返回多个值,引用形参为我们一次返回多个结果提供了有效的途径。

和所有数组一样,当将多维数组传递给函数时,真正传递的是指向数组首元素的指针。因为我们处理的是数组的数组,所以首元素本身就是一个数组,指针就是一个指向数组的指针。数组第二维(以及后面所有维度)的大小都是数组类型的一部分,不能省略

使用迭代器访问元素:

vector<int>::iterator it;
for(it=vec.begin();it!=vec.end();it++)
cout<<*it<<endl;

int *a = new int[10] (); // 每个元素初始化为0,括号内不能写其他值,只能初始化为0

谨记一句话,但凡使用了迭代器的循环体,都不要向迭代器所属的容器添加元素

C++初始化类成员的,它们是按照声 明的顺序初始化的,而不是按照出现在初始化列表中的顺序。

函数的传入参数

要修改变量的值,需要使用变量类型的指针作为参数或者变量的引用。如果变量是一般类型的变量,例如int,则需要使用int 类型的指针类型int * 作为参数或者int的引用类型int&。但是如果变量类型是指针类型,例如char*,那么需要使用该类型的指针,即指向指针的指针类型 char**,或者该类型的引用类型char*&。

常用库或函数使用说明

pugixml

pugixml 是一个用于解析和操作 XML 文档的 C++ 库,它被设计为轻量级、高性能且易于使用。pugixml 提供了简单而强大的 API,使得处理 XML 数据变得简单而高效。以下是关于 pugixml 的介绍和使用方法:

特点和优势:

轻量级: pugixml 是一个轻量级的库,不需要大量的内存开销,因此适用于嵌入式系统和资源受限的环境。
高性能: pugixml 在解析和处理 XML 数据时具有很高的性能,它使用了一些优化策略,如内存池,来提高效率。
易于使用: pugixml 提供了简洁明了的 API,使得读取、写入和操作 XML 数据变得非常容易。
安装和集成:

pugixml 不需要额外的安装过程,它是一个头文件库(header-only library),你只需要将 pugixml 的头文件包含在你的项目中即可开始使用。

使用示例

以下是一个简单的 pugixml 使用示例,展示了如何解析一个 XML 文档并访问其中的元素和属性:

#include <iostream>
#include <string>
#include "pugixml.hpp"

int main() {
pugi::xml_document doc;

// 加载 XML 文件
pugi::xml_parse_result result = doc.load_file("example.xml");
if (!result) {
std::cout << "XML parsing error: " << result.description() << std::endl;
return 1;
}

// 访问根节点
pugi::xml_node root = doc.child("root");
std::cout << "Root node name: " << root.name() << std::endl;

// 遍历子节点
for (pugi::xml_node child : root.children()) {
std::cout << "Child node name: " << child.name() << std::endl;

// 访问节点属性
for (pugi::xml_attribute attr : child.attributes()) {
std::cout << "Attribute name: " << attr.name() << ", value: " << attr.value() << std::endl;
}
}

return 0;
}

上述示例首先加载了一个名为 example.xml 的 XML 文件,然后访问了根节点和其子节点,以及子节点的属性。

知识点

1, 如果文件名用 尖括号括 起来, 表明这个文件是一个工程或标准头文件查找过程会 检查预定义的 目录;如果文件名用一对 引号括 起来则表明该文件是用户提供的头文件查找该文件时将从 当前文件目 录开始.

2, C++ 程序可以定义为对象的集合, 这些对象通过调用彼此的方法进行交互.

3, 块注释符是不可以嵌套使用的.

4, 头文件中定义的操纵符有: endl:输出时插入换行符并刷新流;endls:输出时在字符 插入NULL作为尾符;flush:刷新缓冲区, 把流从缓冲区输出到目标设备, 并清空缓冲区; ws:输入时略去空白字符;dec:令IO数据按十进制格式;hex:令IO数据按十六进制格式; oct:令IO数据按八进制格式

5, 使用 typedef 为一个已有的类型取一个新的名字;

6, 枚举类型(enumeration)是C++中的一种派生数据类型, 它是由用户定义的若干枚举常量的集合. 如果一个变量只有几种可能的值, 可以定义为枚举(enumeration)类型.所谓”枚举”是指将变量的值一一列举出来, 变量的值只能在列举出来的值的范围内.

7, 把常量定义为大写字母形式, 是一个很好的编程实践.

8, const 类型的对象在程序执行期间不能被修改改变.修饰符 volatile 告诉编译器不需要优化volatile声明的变量,让程序可以直接从内存中读取变量.对于一般的变量编译器会对变量进行优化,将内存中的变量值放在寄存器中以加快读写效率.由 restrict 修饰的指针是唯一一种访问它所指向的对象的方式.只有 C99 增加了新的类型限定符 restrict.

9, extern 存储类用于提供一个全局变量的引用,全局变量对所有的程序文件都是可见的.当您使用 ‘extern’ 时,对于无法初始化的变量,会把变量名指向一个之前定义过的存储位置. 当有多个文件且定义了一个可以在其他文件中使用的全局变量或函数时,可以在其他文件中使用 extern 来得到已定义的变量或函数的引用.可以这么理解,extern 是用来在另一个文件中声明一个全局变量或函数.

10, “::”在C++中表示作用域,和所属关系.”::”是运算符中等级最高的

11, 预定义的对象 cin 是 iostream 类的一个实例.cin 对象附属到标准输入设备,通常是键盘.cin 是与流提取运算符 >>

12, array也位于名称空间std中,与数组一样,array对象的长度也是固定的,也使用栈(静态内存分配),而不是自由存储区,因此其效率与数组相同,但更方便,更安全.

13, 类定义是以关键字 class 开头,后跟类的名称.类的主体是包含在一对花括号中.类定义后必须跟着一个分号或一个声明列表.

14, 关键字 public 确定了类成员的访问属性.在类对象作用域内,公共成员在类的外部是可访问的.

15, 点运算符“.”应用于实际的对象,箭头运算符“->”与一个指针对象的指针一起使用.

16, ::std::vector<> 是一个认真设计的值类型,天生是可以拷贝构造和可赋值的.如果 T 是可比较的,那么 ::std::vector 将自动地是可以比较的. 四个特殊的成员函数 T(); // 缺省构造函数(default constructor) ~T(); // 解构函数(destructor) T( T const& ); // 拷贝构造函数 T& operator=( T const& ); // 拷贝赋值函数

17, 容量是指在容器下一次需要增长自己之前能够被加入到容器中的元素的总数连续存储的容器相关.

18, 实际的矩阵是在构造函数中动态分配的,在析构函数中删除的.

19, CLU, Ada 和 Modula-2 是三种支持抽象数据类型的程序设计语言.

20, 条件指示符#ifndef 检查前面是否已经被定义,#ifdef 指示符常被用来判断一个预处理器常量是否已被定义以便有条件地包含程序代码.

21, 不能用赋值操作符把一个数组拷贝到另一个中去.

22, 在C++中,设计 size_t 就是为了适应多个平台的. size_t的引入增强了程序在不同平台上的可移植性. 类似于无符号整形(unsignted int)

23, 每个类对象在被程序最后一次使用之后它的析构函数就会被自动调用.

24, &:取地址,*:间接引用. 预处理命令的操作对象是编译器和连接器。

25, 当指针变量p指向整型变量a时,*p的运算结果就是a本身,而非a的值。

26, 多数情况下,应该在指针间接引用之前检测是否为空指针,从而避免异常错误。

27, p1指向变量a,p2指向变量b,*p1等价于a, *p2等价于b.

28, 不要返回函数里的局部对象的引用。

29, 结构体对象的指针成员存储的是地址,而不是指向的内容名,这一点与数组成员是不同的。

30, 当创建一个类时,您不需要重新编写新的数据成员和成员函数,只需指定新建的类继承了一个已有的类的成员即可。这个已有的类称为基类,新建的类称为派生类.

31, 为了避免同一个头文件被包含(include)多次,C/C++中有两种宏实现方式:一种是#ifndef方式;另一种是#pragma once方式。

32, 有时会遇到这种情况:希望从表达式的类型推断出要定义的变量的类型,但是不想用该表达式的值初始化变量。为了满足这一要求,C++11 新标准引入了第二种类型说明符 decltype,它的作用是选择并返回操作数的数据类型。在此过程中,编译器分析表达式并得到它的类型,却不实际计算表达式的值.

33, 切记:decltype ((variable))(注意是双层括号)的结果永远是引用,而 decltype (variable)结果只有当 variable 本身就是一个引用时才是引用。

34, const是说明这个函数不会修改任何数据成员(object)。 为了声明一个const成员函数, 把const关键字放在函数括号的后面。声明和定义的时候都应该放const关键字。

35, Vector作为函数的参数或者返回值时,需要注意它的写法vector&a, 其中的“&”不能少.

36, 类的构造函数是类的一种特殊的成员函数,它会在每次创建类的新对象时执行。 构造函数的名称与类的名称是完全相同的,并且不会返回任何类型,也不会返回 void。构造函数可用于为某些成员变量设置初始值。

37, 任何不会修改数据成员的函数都应该声明为const类型;

参考

1, 《C++ Primer 第五版》

2, 浅然言而信

3, jackywgw

4, 瘋子朱磊

5, 《高质量C++/C编程指南》—林锐

6, Tham

7, Jessica程序猿

CMakeLists Eigen FCPX GNU Gazebo Git Interest KDL Life Linux Matrix ODE ROS Ros UML Ubuntu VcXsrv algorithm algorithms axis-angle bode calibration chrome control cpp data_struct dots figure gdb latex launch life linux mac math matlab memory motor moveit operator optimal algorithm python robot robotics ros ros2 rtb simulation stl thread tools twist urdf velocity vim web work wsl
知识共享许可协议