C++学习笔记
文章目录
- C++学习笔记
- 1.C++ 迭代器
- 1.1emplace_back()
- 1.1.1用法同push_back()
- 1.1.2emplace_back()
- 2.erase()
- 3.to_string()
- 4.vector容器的resize()和reserve()
- 6.for(auto x : num) 语法
- 6.1关于是否要加 `&`
- 7.内联函数
- 7.1为什么要用内联函数?
- 7.2将内联函数放入头文件
- 7.3 慎用内联
- 8.c++中(:)和(::)的用法
- 8.1(:)的用法
- 8.2双冒号(::)用法
- 9.运算符优先级和结合性一览表
- 一些容易出错的优先级问题
- 10.vector中二维数组的遍历
- 10.1迭代器遍历
- 10.2下标遍历
- 11.NULL和nullptr类型区别
1.C++ 迭代器
-
背景: 指针可以用来遍历存储空间连续的数据结构,但是对于存储空间费连续的,就需要寻找一个行为类似指针的类,来对非数组的数据结构进行遍历。
定义:迭代器是一种检查容器内元素并遍历元素的数据类型。常用迭代器类型如下:
如上图所示,迭代器类型主要支持两类,随机访问和双向访问。其中vector和deque支持随机访问,list,set,map等支持双向访问。
1)随机访问:提供了对数组元素进行快速随机访问以及在序列尾部进行快速插入和删除操作。
2)双向访问:插入和删除所花费的时间是固定的,与位置无关。
1.1emplace_back()
1.1.1用法同push_back()
push_back
是 C++ STL(标准模板库)中 std::vector
容器的一个成员函数,用于在 vector
的末尾添加一个元素。这个函数会扩展 vector
的大小以容纳新的元素,并返回 void
。
当你调用 push_back
时,它会做以下几件事:
- 如果
vector
有足够的容量来容纳新元素,那么它将简单地在末尾添加该元素。 - 如果
vector
没有足够的容量,push_back
将会自动分配更多的内存,并可能将现有元素复制到新的内存位置,然后将新元素添加到末尾。
语法:
void push_back(const value_type& x);
push_back
是向 vector
添加元素的一种非常高效的方式,特别是当你不知道要添加多少元素时,因为它会自动处理内存分配。然而,如果你知道将要添加的元素数量,使用 reserve
函数预先分配足够的内存可以提高性能,因为这样可以减少多次内存分配和复制操作。
x
是要添加到vector
末尾的元素。
1.1.2emplace_back()
emplace_back
是 C++ STL(标准模板库)中的一个成员函数,它属于 std::vector
、std::deque
和 std::list
等序列容器。emplace_back
用于在容器的末尾构造一个新元素,而不是先构造一个元素再将其插入到容器中,这样可以提高效率。
使用 emplace_back
时,你可以直接在容器中构造元素,而不需要使用临时对象。这减少了复制或移动操作,从而提高了性能。emplace_back
接受构造新元素所需的参数,并使用这些参数在容器的末尾就地构造元素。
以下是 emplace_back
的一个使用示例:
#include <vector>
int main() {
std::vector<int> vec;
vec.emplace_back(10); // 在末尾添加一个值为10的整数
vec.emplace_back(); // 添加一个默认构造的整数,其值为0
return 0;
}
2.erase()
有三种用法:
(1)erase(pos, n);
删除从pos开始的n个字符,例如erase( 0, 1),删除0位置的一个字符,即删除第一个字符。
(2)erase(position);
删除position处的一个字符(position是个string类型的迭代器)。
(3)erase(first,last);
删除从first到last之间的字符(first和last都是迭代器)。
#include <iostream>
#include <string>
using namespace std;
int main ()
{
string str ("This is an example phrase.");
string::iterator it;
// 第(1)种用法
str.erase (10,8);
cout << str << endl; // "This is an phrase."
// 第(2)种用法
it=str.begin()+9;
str.erase (it);
cout << str << endl; // "This is a phrase."
// 第(3)种用法
str.erase (str.begin()+5, str.end()-7);
cout << str << endl; // "This phrase."
return 0;
}
3.to_string()
在C++中,to_string
是一个函数模板,它提供了一种将数值类型转换为字符串的方法。to_string
函数属于 <sstream>
和 <iomanip>
头文件中的 <<string>>
命名空间,因此在使用之前需要包含这些头文件。
to_string
函数可以处理多种数值类型,包括整数类型(如 int
、long
、long long
等)和浮点数类型(如 float
、double
等)。它将数值转换为一个 std::string
类型的对象。
以下是 to_string
函数的一些使用示例:
#include <iostream>
#include <string> // for std::string and std::to_string
int main() {
int i = 123;
double d = 456.789;
char c = 'a';
// 转换整数为字符串
std::string str1 = std::to_string(i);
std::cout << "Integer as string: " << str1 << std::endl;
// 转换浮点数为字符串
std::string str2 = std::to_string(d);
std::cout << "Double as string: " << str2 << std::endl;
// 转换字符为字符串
std::string str3 = std::to_string(c);
std::cout << "Character as string: " << str3 << std::endl;
return 0;
}
输出将会是:
Integer as string: 123
Double as string: 456.789
Character as string: a
4.vector容器的resize()和reserve()
vector 的 reserve 增加了 vector 的 capacity ,但是它的 size 没有改变!而 resize 改变了 vector 的 capacity 同时也增加了它的 size !
原因如下:
reserve是容器预留空间,但在空间内不真正创建元素对象,所以在没有添加新的对象之前,不能引用容器内的元素。 加入新的元素时,要调用push_back() / insert()函数。resize是改变容器的大小,且在创建对象,因此,调用这个函数之后,就可以引用容器内的对象了, 因此当加入新的元素时,用operator[]操作符,或者用迭代器来引用元素对象。此时再调用push_back()函数,是加在这个新的空间后面的。
下面是这两个函数使用例子:
不管是调用resize还是reserve,二者对容器原有的元素都没有影响。
vector<int> myVec;
myVec.reserve(100); // 新元素还没有构造,
// 此时不能用[]访问元素
for (int i = 0; i < 100; i++)
{
myVec.push_back(i); //新元素这时才构造
}
myVec.resize(102); // 用元素的默认构造函数构造了两个新的元素
myVec[100] = 1; //直接操作新元素
myVec[101] = 2;
例子2:
#include <vector>
#include <iostream>
using namespace std;
int main(int argc, char* argv[])
{
vector<int> vect;
vect.push_back(1);
vect.push_back(2);
vect.push_back(3);
vect.push_back(4);
vect.reserve(100);
cout << vect.size() << endl; //size为4,但是capacity为100
int i = 0;
for (i = 0; i < 104; i++)
{
cout<<vect[i]<<endl;
}
return 0;
}
例子3:
#include <vector>
#include <iostream>
using namespace std;
int main(int argc, char* argv[])
{
vector<int> vect;
vect.push_back(1);
vect.push_back(2);
vect.push_back(3);
vect.push_back(4);
vect.resize(100); //新的空间不覆盖原有四个元素占有的空间,现在size和capacity都是100
cout<<vect.size()<<endl;
int i = 0;
for (i = 0; i < 104; i++)
{
cout<<vect[i]<<endl;
}
return 0;
}
例子4:
#include <vector>
#include <iostream>
using namespace std;
int main(int argc, char* argv[])
{
vector<int> vect;
vect.resize(100); //分配100个空间
vect.push_back(1);
vect.push_back(2);
vect.push_back(3);
vect.push_back(4);
cout << vect.size() <<endl; //现在size和capacity都是104
int i = 0;
for (i = 0; i < 104; i++)
{
cout<<vect[i]<<endl;
}
return 0;
}
6.for(auto x : num) 语法
C++11新增了一种循环:基于范围的for循环,这简化了一种常见的循环任务:对数组和容器类的每个元素都执行相同的操作
for (auto x : nums)
作用就是迭代容器中所有的元素,每一个元素的临时名字就是x,等同于下边代码
for (vector<int>::iterator iter = nums.begin(); iter != nums.end(); iter++)
语法形式
for(数据类型 变量 : 序列)
循环语句
-
序列
-
可以是花括号括起来的初始值列表、数组、
vector
、string
,这些类型的特点是拥有能返回迭代器的begin
和end
成员 -
数据类型:变量
-
序列中的每个元素都能转换成该变量的类型,最简单的方法是使用
auto
类型说明符。 -
若需要对序列中的元素进行写操作,则需要声明成引用类型
&
。
【示例1】
#include <iostream>
using namespace std;
int main(){
int num[5]={1,2,3,4,5};
for(auto x:num)
cout<<x<<endl;
return 0;
}
【示例2】
#include <iostream>
using namespace std;
int main(){
string s;
cin>>s;
cout<<s<<endl;
for (auto &c : s)
c=toupper(c);
cout<<s<<endl;
}
toupper 函数用于将小写字母转换为大写字母。它是 <cctype> 头文件中定义的一个标准库函数。toupper 函数接受一个 char 类型的参数,并返回转换后的字符。如果输入的字符不是小写字母,它将原样返回该字符。
【示例3】
#include <iostream>
using namespace std;
int main(){
for(auto x:{1,2,3,4,5})
cout<<x<<endl;
}
【示例4】下面的例子将vector对象中的每个元素都翻倍
vector<int>v={0,1,2,3,4};
//因为要对v中的元素进行写操作,所以是引用类型
for(auto &r : v)
r*= 2;
6.1关于是否要加 &
- 不加引用是取值,加引用是取引用(相当于指针)。
- 如果数组中的对象不大(比如 int,char 等),以及明确要求不修改数组本身,则可以不加引用,每次取值拷贝。
- 如果数组中的对象很大,或者想要修改,则需要取引用。
7.内联函数
7.1为什么要用内联函数?
在C++中我们通常定义以下函数来求两个整数的最大值:
int max(int a, int b)
{
return a > b ? a : b;
}
为这么一个小的操作定义一个函数的好处有:
① 阅读和理解函数 max 的调用,要比读一条等价的条件表达式并解释它的含义要容易得多
② 如果需要做任何修改,修改函数要比找出并修改每一处等价表达式容易得多
③ 使用函数可以确保统一的行为,每个测试都保证以相同的方式实现
④函数可以重用,不必为其他应用程序重写代码
虽然有这么多好处,但是写成函数有一个潜在的缺点:调用函数比求解等价表达式要慢得多。在大多数的机器上,调用函数都要做很多工作:调用前要先保存寄存器,并在返回时恢复,复制实参,程序还必须转向一个新位置执行
C++中支持内联函数,其目的是为了提高函数的执行效率,用关键字 inline 放在函数定义(注意是定义而非声明,下文继续讲到)的前面即可将函数指定为内联函数,内联函数通常就是将它在程序中的每个调用点上“内联地”展开,假设我们将 max 定义为内联函数:
inline int max(int a, int b)
{
return a > b ? a : b;
}
则调用: cout << max(a, b) << endl;
在编译时展开为: cout << (a > b ? a : b) << endl; 从而消除了把 max写成函数的额外执行开销。
7.2将内联函数放入头文件
关键字 inline 必须与函数定义体放在一起才能使函数成为内联,仅将 inline 放在函数声明前面不起任何作用。
如下风格的函数 Foo 不能成为内联函数:
inline void Foo(int x, int y); // inline 仅与函数声明放在一起
void Foo(int x, int y)
{
//...
}
而如下风格的函数 Foo 则成为内联函数:
void Foo(int x, int y);
inline void Foo(int x, int y) // inline 与函数定义体放在一起
定义在类声明之中的成员函数将自动地成为内联函数,例如:
class A
{
public:
void Foo(int x, int y) { ... } // 自动地成为内联函数
}
但是编译器是否将它真正内联则要看 Foo函数如何定义
内联函数应该在头文件中定义,这一点不同于其他函数。编译器在调用点内联展开函数的代码时,必须能够找到 inline 函数的定义才能将调用函数替换为函数代码,而对于在头文件中仅有函数声明是不够的。
当然内联函数定义也可以放在源文件中,但此时只有定义的那个源文件可以用它,而且必须为每个源文件拷贝一份定义(即每个源文件里的定义必须是完全相同的),当然即使是放在头文件中,也是对每个定义做一份拷贝,只不过是编译器替你完成这种拷贝罢了。但相比于放在源文件中,放在头文件中既能够确保调用函数是定义是相同的,又能够保证在调用点能够找到函数定义从而完成内联(替换)。
但是你会很奇怪,重复定义那么多次,不会产生链接错误?
我们来看一个例子:
// 文件A.h 代码如下:
class A
{
public:
A(int a, int b) : a(a),b(b){}
int max();
private:
int a;
int b;
};
// 文件A.cpp 代码如下:
#include "A.h"
inline int A::max()
{
return a > b ? a : b;
}
// 文件Main.cpp 代码如下:
#include <iostream>
#include "A.h"
using namespace std;
inline int A::max()
{
return a > b ? a : b;
}
int main()
{
A a(3, 5);
cout << a.max() << endl;
return 0;
}
一切正常编译,输出结果:5
倘若你在Main.cpp中没有定义max内联函数,那么会出现链接错误:
error LNK2001: unresolved external symbol "public: int __thiscall A::max(void)" (?max@A@@QAEHXZ)main.obj
找不到函数的定义,所以内联函数可以在程序中定义不止一次,只要 inline 函数的定义在某个源文件中只出现一次,而且在所有源文件中,其定义必须是完全相同的就可以。
在头文件中加入或修改 inline 函数时,使用了该头文件的所有源文件都必须重新编译。
7.3 慎用内联
“如果所有的函数都是内联函数,还用得着“内联”这个关键字吗?
内联是以代码膨胀(复制)为代价,仅仅省去了函数调用的开销,从而提高函数的执行效率。如果执行函数体内代码的时间,相比于函数调用的开销较大,那么效率的收获会很少。另一方面,每一处内联函数的调用都要复制代码,将使程序的总代码量增大,消耗更多的内存空间。以下情况不宜使用内联:
(1)如果函数体内的代码比较长,使用内联将导致内存消耗代价较高。
(2)如果函数体内出现循环,那么执行函数体内代码的时间要比函数调用的开销大。
8.c++中(:)和(::)的用法
8.1(:)的用法
(1)表示机构内位域的定义(即该变量占几个bit空间)。
typedef struct _XXX{
unsigned char a:4;
unsigned char c;
}; XXX
(2)构造函数后面的冒号起分割作用,是类给成员变量赋值的方法,初始化列表,更适用于成员变量的常量const型。
struct _XXX{
_XXX() : y(0xc0) {}
};
(3)public:
和private:
后面的冒号,表示后面定义的所有成员都是公有或私有的,直到下一个"public:”
或"private:”
出现为止。"private:"
为默认处理。
(4)类名冒号后面的是用来定义类的继承。
class 派生类名 : 继承方式 基类名
{
派生类的成员
};
继承方式:public、private和protected,默认处理是public。
8.2双冒号(::)用法
(1)表示“域操作符”
- 例:声明了一个类A,类A里声明了一个成员函数
void f()
,但没有在类的声明里给出f的定义,那么在类外定义f时, 就要写成void A::f()
,表示这个f()
函数是类A的成员函数。
(2)直接用在全局函数前,表示是全局函数
- 例:在VC里,你可以在调用API 函数里,在API函数名前加
::
(3)表示引用成员函数及变量,作用域成员运算符
- 例:
System::Math::Sqrt()
相当于System.Math.Sqrt()
9.运算符优先级和结合性一览表
优先级 | 运算符 | 名称或含义 | 使用形式 | 结合方向 | 说明 |
---|---|---|---|---|---|
1 | [] | 数组下标 | 数组名[常量表达式] | 左到右 | |
() | 圆括号 | (表达式) 函数名(形参表) | |||
. | 成员选择(对象) | 对象.成员名 | |||
-> | 成员选择(指针) | 对象指针->成员名 | |||
2 | - | 负号运算符 | -表达式 | 右到左 | 单目运算符 |
(类型) | 强制类型转换 | (数据类型)表达式 | |||
++ | 自增运算符 | ++变量名 变量名++ | 单目运算符 | ||
– | 自减运算符 | –变量名 变量名– | 单目运算符 | ||
* | 取值运算符 | *指针变量 | 单目运算符 | ||
& | 取地址运算符 | &变量名 | 单目运算符 | ||
! | 逻辑非运算符 | !表达式 | 单目运算符 | ||
~ | 按位取反运算符 | ~表达式 | 单目运算符 | ||
sizeof | 长度运算符 | sizeof(表达式) | |||
3 | / | 除 | 表达式 / 表达式 | 左到右 | 双目运算符 |
* | 乘 | 表达式*表达式 | 双目运算符 | ||
% | 余数(取模) | 整型表达式%整型表达式 | 双目运算符 | ||
4 | + | 加 | 表达式+表达式 | 左到右 | 双目运算符 |
- | 减 | 表达式-表达式 | 双目运算符 | ||
5 | << | 左移 | 变量<<表达式 | 左到右 | 双目运算符 |
>> | 右移 | 变量>>表达式 | 双目运算符 | ||
6 | > | 大于 | 表达式>表达式 | 左到右 | 双目运算符 |
>= | 大于等于 | 表达式>=表达式 | 双目运算符 | ||
< | 小于 | 表达式<表达式 | 双目运算符 | ||
<= | 小于等于 | 表达式<=表达式 | 双目运算符 | ||
7 | == | 等于 | 表达式==表达式 | 左到右 | 双目运算符 |
!= | 不等于 | 表达式!= 表达式 | 双目运算符 | ||
8 | & | 按位与 | 表达式&表达式 | 左到右 | 双目运算符 |
9 | ^ | 按位异或 | 表达式^表达式 | 左到右 | 双目运算符 |
10 | | | 按位或 | 表达式|表达式 | 左到右 | 双目运算符 |
11 | && | 逻辑与 | 表达式&&表达式 | 左到右 | 双目运算符 |
12 | || | 逻辑或 | 表达式||表达式 | 左到右 | 双目运算符 |
13 | ?: | 条件运算符 | 表达式1? 表达式2: 表达式3 | 右到左 | 三目运算符 |
14 | = | 赋值运算符 | 变量=表达式 | 右到左 | |
/= | 除后赋值 | 变量/=表达式 | |||
*= | 乘后赋值 | 变量*=表达式 | |||
%= | 取模后赋值 | 变量%=表达式 | |||
+= | 加后赋值 | 变量+=表达式 | |||
-= | 减后赋值 | 变量-=表达式 | |||
<<= | 左移后赋值 | 变量<<=表达式 | |||
>>= | 右移后赋值 | 变量>>=表达式 | |||
&= | 按位与后赋值 | 变量&=表达式 | |||
^= | 按位异或后赋值 | 变量^=表达式 | |||
|= | 按位或后赋值 | 变量|=表达式 | |||
15 | , | 逗号运算符 | 表达式,表达式,… | 左到右 |
上表中可以总结出如下规律:
- 结合方向只有三个是从右往左,其余都是从左往右。
- 所有双目运算符中只有赋值运算符的结合方向是从右往左。
- 另外两个从右往左结合的运算符也很好记,因为它们很特殊:一个是单目运算符,一个是三目运算符。
- C语言中有且只有一个三目运算符。
- 逗号运算符的优先级最低,要记住。
- 此外要记住,对于优先级:算术运算符 > 关系运算符 > 逻辑运算符 > 赋值运算符。逻辑运算符中“逻辑非 !”除外。
一些容易出错的优先级问题
上表中,优先级同为1 的几种运算符如果同时出现,那怎么确定表达式的优先级呢?这是很多初学者迷糊的地方。下表就整理了这些容易出错的情况:
优先级问题 | 表达式 | 经常误认为的结果 | 实际结果 |
---|---|---|---|
. 的优先级高于 *(-> 操作符用于消除这个问题) | *p.f | p 所指对象的字段 f,等价于: (*p).f | 对 p 取 f 偏移,作为指针,然后进行解除引用操作,等价于: *(p.f) |
[] 高于 * | int *ap[] | ap 是个指向 int 数组的指针,等价于: int (*ap)[] | ap 是个元素为 int 指针的数组,等价于: int *(ap []) |
函数 () 高于 * | int *fp() | fp 是个函数指针,所指函数返回 int,等价于: int (*fp)() | fp 是个函数,返回 int*,等价于: int* ( fp() ) |
== 和 != 高于位操作 | (val & mask != 0) | (val &mask) != 0 | val & (mask != 0) |
== 和 != 高于赋值符 | c = getchar() != EOF | (c = getchar()) != EOF | c = (getchar() != EOF) |
算术运算符高于位移 运算符 | msb << 4 + lsb | (msb << 4) + lsb | msb << (4 + lsb) |
逗号运算符在所有运 算符中优先级最低 | i = 1, 2 | i = (1,2) | (i = 1), 2 逗号表达式的值为2 |
10.vector中二维数组的遍历
10.1迭代器遍历
void reverse_iterator(vector<vector<int>> vec)
{
vecotr<int>::iterator it;
vector<vector<int>>::iterator iter;
vector<int> vec_tmp;
for (iter = vec.begin(); iter != vec.end(); iter++)
{
vec_tmp = *iter;
for (it = vec_tmp.begin(); it != vec_tmp.end(); it++)
cout << *it << " ";
cout << endl;
}
}
void reverse_iterator(vector<vector<int>> vec)
{
for (vector<vector<int>>::iterator it = vec.begin(); it != vec.end(); it++)
for (vector<int>:: iterator iter = (*it).begin(); iter != (*it).end(); iter++)
{
cout << *iter << " ";
}
}
10.2下标遍历
void reverse_index(vector<vector<int>> vec)
{
for (int i = 0; i < vec.size(); i++)
{
for (int j = 0; j < vec[i].size(); j++)
cout << vec[i][j] << " ";
cout << endl;
}
}
11.NULL和nullptr类型区别
- nullptr和NULL类型区别
-
NULL是一个无类型的东西,而且是一个宏。
- 在C中,习惯将NULL定义为void*指针值0,但同时,也允许将NULL定义为整常数0
- 在C++中,NULL却被明确定义为整常数0
-
nullptr是有类型的(放了在stddef头文件中),类型是 typdef decltype(nullptr) nullptr_t;
- C++中NULL使用存在的问题
- 在c语言环境下,由于不存在函数重载等问题,直接将NULL定义为一个void*的指针就可以完美的解决一切问题。
- 在c++环境下情况就复杂起来, 首先我们将void*直接赋值给一个指针是不合法的,编译器会报错。有重载或者模板推导的时候,编译器就无法给出正确结果。
- 根本原因和C++的重载函数有关。C++通过搜索匹配参数的机制,试图找到最佳匹配(best-match)的函数,而如果继续支持void*的隐式类型转换,则会带来语义二义性(syntax ambiguous)的问题。
- nullptr的应用:
- 如果我们的编译器是支持nullptr的话,那么我们应该直接使用nullptr来替代NULL的宏定义。正常使用过程中他们是完全等价的。
- 0(NULL)和nullptr可以交换使用
- 不能将nullptr赋值给整形