重学C++:笔记C++基础容器&C++指针引用

Posted TiercelChow

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了重学C++:笔记C++基础容器&C++指针引用相关的知识,希望对你有一定的参考价值。

第4章 C++基础容器

4.1 序列容器–数组

off-by-one error数组下标

off-by-one error (差一错误)

数组中通过左闭右开的方式可以避免上述错误,如:

for (int i = 0; i <10 ; i++) {
    cout << a[i] << endl;
}

数组设计的原则

从0开始,使用非对称空间:

让下界(左侧)可以取到值,让上界(右侧)取不到值;

好处:

  • 取值范围的大小:上界-下界
  • 如果取值范围为空,上界值==下界值
  • 及时取值范围为空上界值也永远不可能小于下界值

4.2 数组的增删改查及二维数组

二维数组设计的tips(循环时尽可能要满足空间局限性)

  • 在一个小的时间窗口内,访问的变量地址越接近越好,这样执行速度快
  • 一般来说,需要将最长的循环放最内层,最短的循环放在最外层,以减少CPU跨层的次数

4.3 动态数组vector

使用前的准备(引入头文件和namespace)

#include <vector>
using namespace std;

相关方法

vector<int> vec = {1, 2, 3};
//在尾部插入元素
vec.push_back(4);
//在中间进行元素插入
vec.insert(vec.end()-1, 4); //在尾部前一个位置插入4
//删除尾部元素
vec.pop_back();
//删除中间元素
vec.erase(vec.end()-1); //删除尾部前一个位置的元素
//获取当前容量
vec.capacity();
//获取已经存储的元素个数
vec.size();

4.4 字符串

字符串变量

  • 字符串是以空字符(’\\0’)结束的字符数组
  • 空字符自动添加到字符串的内部表示中
  • 在声明字符串变量时,应该位这个空结束符预留一个额外元素的空间,如:char s [11] = {“helloworld”} ;

Unicode编码

Unicode编码:最初的目的是把世界上的文字都映射到一套字符空间中

  • UTF-8
  • UTF-16
    • UTF-16BE
    • UTF-16LE
  • UTF-32
    • UTF-32BE
    • UTF-32LE

编码错误的根本原因在于编码方式和解码方式的不统一

Windows的文件可能有BOM(byte order mark),如果要在其他平台使用,可以去掉BOM

字符串的指针表示

指针所指的区域能否改变取决于指针指向的那块区域是否为可变的,如若指向的为常量,则不可变,而若指向的区域为变量,那么可以改变

字符串数组数组名定义了之后就不可变,但是数组里的值可变,而定义一个指针,指针变量的值是可变的,但是若其指向的为常量,那么则指针指向的字符串的值不可变

字符串的基本操作

包含在头文件<string.h>中

  1. 字符串长度:strlen(s)

    返回字符串s的长度(s的长度不包括 ’ \\0 ')

    区别sizeof(),sizeof()计算的为占用的空间,strlen()计算的为字符串长度

  2. 字符串比较:strcmp(s1, s2)

    若s1和s2 是相同的,则返回0;

    若s1 < s2 则返回值小于0;

    若s1 > s2 则返回值大于0

  3. 字符串拷贝:strcpy(s1, s2)

    复制字符串s2到字符串s1

  4. 复制指定长度字符串:strncpy(s1, s2, n)

    将字符串s2中前n个字符串拷贝到s1中

  5. 字符串拼接:strcat(s1, s2)

    将字符串s2拼接到s1之后

  6. 查找字符串:strchr(s1, ch)

    指向字符串s1中字符ch的第一次出现的位置

  7. 查找字符串:strstr(s1, s2)

    指向字符串s1中字符串s2的第一次出现的位置

进行底层操作时,为避免编译器错误发出报错,可添加宏:_CRT_SECURE_NO_WARNINGS

缓冲区溢出问题

举例:在进行字符串拼接的操作时,若拼接长度过大而超出原本的长度,那么就有可能把存储区原本存储的信息改变,造成逻辑的改变

解决方法:养成进行边界判断的习惯,另外也存在更安全的API可供调用,如strcpy()更为安全的版本为strcpy_s(),其他几个字符串操作函数的安全版本均为*_s(),例如strcat_s(s1, size, s2,),当s2的长度大于size时,那么会报错,无法运行

string简介

#include<string>
using namespace std;

//定义字符串变量
string s;//定义空字符串
string s = "helloworld";//定义并初始化
string s ( "helloworld" );
string s = string( "helloworld" );

字符串相关函数:

  • 获取字符串的长度

    s.length()//字符串长度
    s.size()//同上
    s.capacity()//字符串s所占空间大小
    
  • 字符串比较:= = ! = > > = < < =

    string s1 = "hello", s2 = "world";
    cout << (s1 == s2) << endl;//返回0
    cout << (s1 != s2) << endl;//返回1
    

字符串的常用操作:

//转换为C风格的字符串
const char *c_str1 = s1.c_str();
//随机访问(获取字符串中某个字符):[]
string s = "hello";
cout << s[0] << endl;
//字符串拷贝:=
string s1 = "hello";
string s2 = s1;
//字符串拷贝:+、+=
string s1 = "hello", s2 = "world";
string s3 = s1 + s2;//s3:helloworld
s1 += s2;//s1:helloworld

总结:string结合了C++的新特性,使用起来比原始的C风格更安全和方便,对性能要求不是特别高的常见情况可以使用

第5章 彻底学会 C++ 指针,引用

5.1 指针的概念

C++中内存单元内容与地址

指针本身就是一个变量,其符合变量定义的基本形式,它存储的是值的地址。对类型T,T*是“到T的指针”类型,一个类型为T*的变量能保存一个类型T的对象的地址

通过一个指针访问它所指向的地址的过程称为间接访问或者引用指针,这个用于执行简介访问的操作符是单目运算符*

一个变量有三个信息:

  • 变量的地址信息
  • 变量所存的信息
  • 变量的类型

左值与右值

左值:编译器为其单独分配了一块存储空间,可以取其地址的,左值可以放在赋值运算符的左边(最常见的情况如函数和数据成员的名字)

右值:指数据本身,不能取到其自身地址,右值只能在赋值运算符右边(右值是没有标识符,不可以取地址的表达式,一般也称之为临时对象)

一般指针数组和指针数组

指针的数组与数组的指针:

  • 指针的数组(定义的是一个包含n个元素数组,数组里存放的都是T类型的指针):T* t[n]
  • 数组的指针(定义了一个指针,指针指向一个包含n个T类型元素的数组):T(*t) [n](注意[]的优先级比较高)

访问数组的指针所指向的数组的元素时,如:(*t)[3]表示指针所指向的数组的下标为3的元素

const与指针

char const *pStr1 = "helloworld";
char* const pStr2 = "helloworld";//pStr2不可改
char const* const pStr3 = "helloworld";//pStr3不可改

关于const修饰符的部分:

  • 看左侧最近的部分
  • 如果左侧没有,则看右侧

如pStr1指针所指向的地址的内容不能变,pStr2指针指向的地址不能变,而pStr3二者均不可改变

指向指针的指针

int a = 123;
int* b = &a;
int** c = &b;//**c相当于*b,即得到a的值(表达式从里向外逐层求值)
//*操作符具有从右向左的结合性

野指针

  • 未初始化和非法指针

    //eg:
    int* a;//仅定义了一个指针,其所指的区域不确定
    *a = 12;
    
    • 定位到一个非法地址,从而终止
    • 定位到一个可以访问的地址,无意修改了它,这样的错误难以捕捉引发的错误可能与原先用于操作的代码完全不相干

    避免方法:用指针进行间接访之前,一定要确保其已经初始化,并被恰当地赋值

  • NULL指针

    一个特殊的指针变量,表示不指向任何东西

    int* a = NULL; 
    

    NULL指针给出一种方法,来表示特定的指针目前未指向任何东西

    注意事项:

    • 对于一个指针,若已经知道将被初始化为什么地址,那么赋给它这个地址值,否则将其设置为NULL
    • 在对任何一个指针进行间接引用前,先判断这个指针是否为空
  • 杜绝“野”指针

    指向“垃圾”内存的指针,if等判断对它们不起作用,因为没有置NULL

    三种情况:

    • 指针变量没有初始化
    • 已经释放不用的指针没有置NULL,如delete和free之后的指针
    • 指针操作超越了变量的作用范围

    注意事项:没有初始化的,不用的或者超出范围的指针将其值置为NULL

5.2 指针的基本操作

&与*操作符

指针类型默认都是四个字节大小,而与其指向的空间的数据类型无关

char ch = 'a';
char* cp = &ch;
//表达式
//*cp
char ch2 = *cp;//作为右值,将a赋给ch2
*cp = 'b';//作为左值,等价于对ch进行操作
//*cp + 1
char ch2 = *cp + 1;//作为右值,将b赋给ch2
*cp + 1 = ...;//作为左值非法
//*(cp + 1)
char ch2 = *(cp + 1);//作为右值,将ch后一个地址的元素的值赋给ch2
*(cp + 1) = 'b';//作为左值,等价于对ch后一个地址的元素进行操作

原始指针的基本运算

++ 与 – 操作符

char* cp2 = ++cp;
/*
mov		eax,dword ptr [cp]
add		eax,1
mov		dword ptr [cp],eax
mov 	ecx,dword ptr [cp]
mov 	dword ptr [cp2],ecx
*/

char* cp3 = cp++;
/*
mov		eax,dword ptr [cp]
mov 	dword ptr [cp3],eax
mov		exc,dword ptr [cp]
add		ecx,1
mov		dword ptr [cp],ecx
*/

// -- 操作同理 add --> sub
*++p = a;
//相当于对p + 1所指的位置进行赋值操作
*p++ = a;
//相当于对p所指位置进行赋值后p的

关于++++,----等运算符

编译器分解程序符号的方法:逐字符读入,若该字符可能组成一个符号,那么读入下一个字符,知道读入的字符不能在组成一个有意义的符号(贪心法)

int a = 1, b = 2, c, d;
c = a +++ b; 	// --> (a ++) + b
d = a ++++ b; 	//error
++*++p;			// --> ++(*(++p))

5.3 CPP程序的存储区域划分

#include "stdafx.h"
#include <string>

int a = 0;                        			//(GVAR)全局初始化区 
int* p1;                   	           	  	//(bss)全局未初始化区 
int main()                            	   	//(text)代码区
{
	int b=1;                               	//(stack)栈区变量 
	char s[] = "abc";                  		//(stack)栈区变量
	int*p2=NULL;                         	//(stack)栈区变量
	char *p3 = "123456";                	//123456\\0在常量区, p3在(stack)栈区
	static int c = 0;                     	//(GVAR)全局(静态)初始化区 
	p1 = new int(10);                      	//(heap)堆区变量
	p2 = new int(20);                     	//(heap)堆区变量
	char* p4 = new char[7];              	//(heap)堆区变量
	strcpy_s(p4, 7, "123456");         		//(text)代码区

	//(text)代码区
	if (p1 != NULL)
	{
		delete p1;
		p1 = NULL;
	}
	if (p2 != NULL)
	{
		delete p2;
		p2 = NULL;
	}
	if (p4 != NULL)
	{
		delete[ ] p4;
		p4 = NULL;
	}
	//(text)代码区
	return 0;                            	//(text)代码区
}
  • main函数内定义的变量存储在栈区,且后定义的地址更低;
  • new所申请的区域为堆区,后申请的地址更高;
  • 全局变量和static修饰的变量存在全局区
  • 指针指向内容是否可以改变取决于所指向的区域,常量区的值不可改变,栈区可以改变;
  • 其他代码一般存在代码区;

5.4 CPP动态分配和回收原则

动态分配资源:堆(heap)

程序通常需要涉及三个内存管理器的操作:

  1. 分配一个某个大小的内存块;
  2. 释放一个之前分配的内存块;
  3. 垃圾收集操作。寻找不再使用的内存块并予以释放;

回收策略需要实现性能、实时性、额外开销等方面的平衡,很难有统一和高效的做法。

C++做了1,2;而Java做了1,3。

5.5 RAII初步

资源管理方案:RAII(Resource Acquisition Is Initialization)

RAII依托析构函数,来对所有的资源:包括堆内存在内进行管理。对RAII的使用,使得C++不需要类似于Java那样的垃圾收集方法,也能有效地对内存进行管理。RAII 的存在,也是垃圾收集虽然理论上可以在C++使用,但从来没有真正流行过的主要原因。

RAII比较成熟的智能指针代表:

std::auto_ptr
boost::shared_ptr

5.6 C++中几种变量的对比

栈和堆中的变量对比

全局静态存储区和常量存储区的变量对比

5.7 内存泄漏

什么是内存泄漏问题(memory leak)

指程序中己动态分配的堆内存由于某种原因程序未释放或无法释,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。

内存泄漏发生原因和排查方式

  1. 内存泄漏主要发生在堆内存分配方式中,即“配置了内存后,所有指
    向该内存的指针都遗失了”。若缺乏语言这样的垃圾回收机制,这样的内存
    片就无法归还系统。
  2. 因为内存泄漏属于程序运行中的问题,无法通过编译识别,所以
    只能在程序运行过程中来判别和诊断。

注意delete 和delete[] 的区别。

5.8 智能指针

使用指针是非常危险的行为,可能存在空指针,野指针问题,并可能造成内存泄漏问题。

可指针又非常的高效,所以我们希望以更安全的方式来使用指针。

两种方案:

  • 使用更安全的指针:智能指针
  • 不使用指针,使用更安全的方式:引用

C+ +中推出了四种常用的智能指针:
unique_ ptr、 shared ptr、weak_ ptr 和C++ 11中已经废弃(deprecated)的auto_ ptr,在C++ 17中被正式删除。

auto_ptr

由new expression获得对象,在auto_ ptr 对象销毁时,他所管理的对象也会
自动被delete掉。

所有权转移:不小心把它传递给另外的智能指针,原来的指针就不再拥有这个对象了。在拷贝/赋值过程中,会直接剥夺指针对原对象对内存的控制权,转交给新对象,然后再将原对象指针置为nullptr。

演示代码:

#include <string>
#include <iostream>
#include <memory>
using namespace std;
int main()
{
	{// 确定auto_ptr失效的范围
		// 对int使用
		auto_ptr<int> pI(new int(10));
		cout << *pI << endl;                // 10 
		// auto_ptr	C++ 17中移除	拥有严格对象所有权语义的智能指针
		// auto_ptr原理:在拷贝 / 赋值过程中,直接剥夺原对象对内存的控制权,转交给新对象,
		// 然后再将原对象指针置为nullptr(早期:NULL)。这种做法也叫管理权转移。
		// 他的缺点不言而喻,当我们再次去访问原对象时,程序就会报错,所以auto_ptr可以说实现的不好,
		// 很多企业在其库内也是要求不准使用auto_ptr。
		auto_ptr<string> languages[5] = {
			auto_ptr<string>(new string("C")),
			auto_ptr<string>(new string("Java")),
			auto_ptr<string>(new string("C++")),
			auto_ptr<string>(new string("Python")),
			auto_ptr<string>(new string("Rust"))
		};
		cout << "There are some computer languages here first time: \\n";
		for (int i = 0; i < 5; ++i)
		{
			cout << *languages[i] << endl;
		}
		auto_ptr<string> pC;
		pC = languages[2]; // languges[2] loses ownership. 将所有权从languges[2]转让给pC,
		//此时languges[2]不再引用该字符串从而变成空指针
		cout << "There are some computer languages here second time: \\n";
		for (int i = 0; i < 2; ++i)
		{
				cout << *languages[i] << endl;
		}
		cout << "The winner is " << *pC << endl;
		//cout << "There are some computer languages here third time: \\n";
		//for (int i = 0; i < 5; ++i)
		//{
		//	cout << *languages[i] << endl;
		//}
	}
	return 0; 
}

unique_ptr

unique_ptr是专属所有权,所以unique_ptr管理的内存,只能被一个对象持有,不支持复制和赋值。

移动语义: unique_ptr禁止了拷贝语义,但有时我们也需要能够转移所有权,于是提供了移动语义,即可以使用std::move()进行控制所有权的转移。

#include <memory>
#include <iostream>
using namespace std;
int main()
{
	// 在这个范围之外,unique_ptr被释放
	{
		auto i = unique_ptr<int>(new int(10));
		cout << *i << endl;
	}

	// unique_ptr
	auto w = std::make_unique<int>(10);
	cout << *(w.get()) << endl;                             // 10
	//auto w2 = w; // 编译错误如果想要把 w 复制给 w2, 是不可以的。
	//  因为复制从语义上来说,两个对象将共享同一块内存。

	// unique_ptr 只支持移动语义, 即如下
	auto w2 = std::move(w); // w2 获得内存所有权,w 此时等于 nullptr
	cout << ((w.get() != nullptr) ? (*w.get()) : -1) << endl;       // -1
	cout << ((w2.get() != nullptr) ? (*w2.get()) : -1) << endl;   // 10
    return 0;
}

shared_ptr

shared_ptr通过一个引用计数共享一个对象.

shared_ ptr 是为了解决auto_ ptr在对象所有权上的局限性,在使用引用计数的机制上提供了可以共享所有权的智能指针,当然这需要额外的开销。

通过引用计数对引用个数进行记录,当引用计数为0时,该对象没有被使用,可以进行析构。

#include <iostream>
#include <memory>
using namespace std;
int main()
{
	// shared_ptr 
	{
		//shared_ptr 代表的是共享所有权,即多个 shared_ptr 可以共享同一块内存。
		auto wA = shared_ptr<int>(new int(20));
		{
			auto wA2 = wA;
			cout << ((wA2.get() != nullptr) ? (*wA2.get()) : -1) << endl;   // 20
			cout << ((wA.get() != nullptr) ? (*wA.get()) : -1) << endl;     // 20
			cout << wA2.use_count() << endl;                                // 2
			cout << wA.use_count() << endl;                                 // 2
		}
		cout << wA2.use_count() << endl;                                               
		cout << wA.use_count() << endl;                                     // 1
		cout << ((wA.get() != nullptr) ? (*wA.get()) : -1) << endl;         // 20
		//shared_ptr 内部是利用引用计数来实现内存的自动管理,每当复制一个 shared_ptr,
		//	引用计数会 + 1。当一个 shared_ptr 离开作用域时,引用计数会 - 1。
		//	当引用计数为 0 的时候,则 delete 内存。
	}

	// move 语法
	auto wAA = std::make_shared<int>(30);
	auto wAA2 = std::move(wAA); // 此时 wAA 等于 nullptr,wAA2.use_count() 等于 1
	cout << ((wAA.get() != nullptr) ? (*wAA.get()) : -1) << endl;           // -1
	cout << ((wAA2.get() != nullptr) ? (*wAA2.get()) : -1) << endl;         // 30
	cout << wAA.use_count() << endl;                                        // 0
	cout << wAA2.use_count() << endl;                                       // 1
	//将 wAA 对象 move 给 wAA2,意味着 wAA 放弃了对内存的所有权和管理,此时 wAA对象等于 nullptr。
	//而 wAA2 获得了对象所有权,但因为此时 wAA 已不再持有对象,因此 wAA2 的引用计数为 1。

    return 0;
}

带来的问题:循环引用

循环引用会导致堆里的内存无法正常回收,造成内存泄漏。

weak_ptr

weak_ ptr 被设计为与shared_ ptr 共同工作,用- -种观察者模式工作。

作用是协助shared_ptr 工作,可获得资源的观测权,像旁观者那样观测资源的使用情况。

观察者意味着weak_ ptr只对shared_ ptr 进行引用,而不改变其引用计数,当被观察的shared_ ptr 失效后,相应的weak_ ptr 也相应失效。

#include <string>
#include <iostream>
#include <memory>
using namespace std;

struct B;
struct A {
	shared_ptr<B> pb;
	~A()
	{
		cout << "~A()" << endl;
	}
};
struct B {
	shared_ptr<A> pa;
	~B()
	{
		cout << "~B()" << endl;
	}
};

// pa 和 pb 存在着循环引用,根据 shared_ptr 引用计数的原理,pa 和 pb 都无法被正常的释放。
// weak_ptr 是为了解决 shared_ptr 双向引用的问题。
struct BW;
struct AW
{
	shared_ptr<BW> pb;
	~AW()
	{
		cout << "~AW()" << endl;
	}
};
struct BW
{
	weak_ptr<AW> pa;
	~BW()
	{
		cout << "~BW()" << endl;
	}
};

void Test()
{
	cout << "Test shared_ptr and shared_ptr:  " << endl;
	shared_ptr<A> tA(new A());                                               // 1
	shared_ptr<B> tB(new B());                                               // 1
	cout << tA.use_count() << endl;
	cout << tB.use_count() << endl;
	tA->pb = tB;
	tB->pa = tA;
	cout << tA.use_count() << endl;                                         // 2 
	cout << tB.use_count() << endl;                                          // 2
}
void Test2()
{
	cout << "Test weak_ptr and shared_ptr:  " << endl;
	shared_ptr<AW> tA(new AW());
	shared_ptr<BW> tB(new BW());
	cout << tA.use_count() << endl;                                          // 1 
	cout << tB.use_count() << endl;                                          // 1
	tA->pb = tB;
	tB->pa = tA;
	cout << tA.use_count() << endl;                                          // 1
	cout << tB.use_count() << endl;                                          // 2
}

int main()
{
	Test();
	Test2();
    return 0;
}

5.9 引用

引用:一种特殊的指针,不允许修改的指针。

使用指针有哪些坑:

  1. 空指针;
  2. 野指针;
  3. 不知不觉改变了指针的值,却继续使用;

使用引用,则可以:

  1. 不存在空引用;
  2. 必须初始化;
  3. 一个引用永远指向它初始化的那个对象;

引用的基本使用:可以认为是指定变量的别名,使用时可以认为是变量本身

示例:

int x = 1, x2 = 3;
int& rx = x;
rx = 2;
cout << x << endl;		// 2
cout << rx << endl;		// 2
rX = x2;
cout << x << endl;		// 3
cout << rx << endl;		// 3

两个问题

有了指针为什么还需要引用?

Bjarne Stroustrup的解释:为了支持函数运算符重载;

有了引用为什么还需要指针?

Bjarne Stroustrup的解释:为了兼容C语言。

补充:关于函数传递参数类型的说明

  • 对内置基础类型(如int,double等)而言:

    在函数中传递时pass by value更高效;

  • 对OO面向对象中自定义类型而言:

    在函数中传递时pass by reference to const更高效。

以上是关于重学C++:笔记C++基础容器&C++指针引用的主要内容,如果未能解决你的问题,请参考以下文章

重学C++:笔记C++基础容器&C++指针引用

重学C++:笔记C++基础容器&C++指针引用

重学C++:笔记C++概括&基础语法&运算符与表达式

重学C++:笔记C++概括&基础语法&运算符与表达式

重学C++:笔记C++概括&基础语法&运算符与表达式

重学C++:笔记C++概括&基础语法&运算符与表达式