转载
最近更新: 2022/06/20 00:02

C++学习笔记 之 基础篇

系列目录

C++学习笔记 之 基础篇

C++学习笔记 之 面向对象篇

C++学习笔记 之 C++11新特性


基础概念

指针

指针的本质

编译器将指针的值翻译成一个地址。因此允许用*运算符(后面称为解引用)取用对应内存空间的值。但如果指向的内存空间是非法的,取值会失败。

指针的核心

编译器根据指针的类型确定指针的宽度(++或--后地址的跨度),获取相应宽度内存空间中保存的值。

因此,指针在声明时决定的指针类型并不重要,可以通过类型转换来进行任意地解释。类型转换之后,指针的宽度可能会有所不同,解引用的结果也发生变化。

关于delete关键字的注意事项

delete关键字会释放指针指向的那一片内存,但是指针本身依旧保存着那一块内存的地址。因此,将指针delete之后,需要将其置为空指针。否则,如果后续对该指针进行了误操作,就有可能会造成内存的非法访问,或者重复delete。

int* p = new int(3);
delete p;
p = nullptr;	//置空,防止后续误操作

引用

引用的优势

  • 值传递:将对应内存空间的值拷贝一份传入函数,函数内部无法修改对应内存空间。对于类对象,值传递需要调用构造函数和析构函数

  • 指针传递:将对应内存空间地址传入函数,函数内部可以随意修改内存空间。如果某个操作误修改了指针本身,可能会造成非法访问,不安全。

  • 引用传递:可以看做是安全的指针传递。因为引用类型只能初始化,不能重新赋值。

引用就是编译器维护的一个指针常量。当我们使用引用来访问变量的时候,编译器会自动帮我们进行内存访问(解引用)。

返回值为引用类型

不返回引用类型的情况下,编译器会生成一个临时变量来存储返回值,同时还要经过析构过程。使用引用类型则可以提高效率。一般而言,返回值为引用类型有两种情况:

  • 加const:返回值为右值,只能用来作为操作数。常用于返回类成员的引用。

  • 不加const:返回值为左值,可以被赋值。可用于操作符重载,如:下标操作符[]的重载,可以直接修改对应元素。

不能返回局部变量的引用,因为会被销毁!

不要返回new对象的引用,这种情况下请使用指针!


static关键字

含义是“静态”,即只初始化一次,具有覆盖整个程序的生命周期;其对应的概念是“非静态”,即动态创建,动态释放。

其作用域仅限于(全局)文件内部或者(局部)对应代码块内部。在作用域之外无法访问,但依旧存在。

  • static用于修饰局部变量,其会在main函数执行前在数据段,而不是堆栈中分配内存,并在第一次声明时初始化。

  • static用于修饰全局变量,会取消全局变量的外部性,其他文件将不能访问该全局变量。

  • static用于修饰成员函数/成员变量,则这个函数/变量不再依赖于类实例,可以用类名来访问。

    • 类的静态成员变量需要在类外单独分配空间(初始化)

    • 只有静态成员函数能修改静态成员变量。

    • 静态成员函数参数只能修改静态成员变量,因为不会传入this指针作为隐含的参数。


STL容器

容器分类

顺序容器

Vector:可变长动态数组,底层实现是数组。支持随机查找,可以在尾部快速地添加或删除元素。

array:数组的上位替代,vector的劣化版。可以忽略

  • 对于二维数组,vector每行可以不等长,但是array每行必须等长。

deque:双端队列,底层实现是数组。支持随机查找,可以在两端快速插入或删除元素。

list:双向链表。可以快速插入或删除元素,不支持随机查找。

关联容器

set:集合,底层实现是红黑树,元素按大小排列。查找、插入、删除的复杂度是log(n)。不允许重复元素。

  • multiset:允许重复元素的set。

unordered_set:无序集合,底层实现是哈希表,元素不按照大小排列。查找、插入、删除的复杂度是log(1)。不允许重复元素。

map:映射,底层实现是红黑树。查找、插入、删除的复杂度是log(n)。不允许重复元素。

  • multimap:允许重复元素的map。

unordered_map:无序映射,底层实现是哈希表,元素不按照大小排列。查找、插入、删除的复杂度是log(1)。不允许重复元素。

容器适配器

stack:栈,底层默认实现是deque。只实现了尾端的push、pop、top操作。

queue:队列,底层默认实现是deque。只实现了队尾push/emplace、队头pop和front操作。

priority_queue:优先队列,底层默认实现是vector和最小堆。实现了pop、push、top方法。

//声明,可以指定底层实现和比较方法
//比较方法默认是less<int>最大堆,greater<int>则是最小堆,也可以自定义
/*自定义比较器,其本质是仿函数
struct cmp {
    //重载了()方法
    bool operator()(int a, int b) {
        return a > b;
    }
};
*/
priority_queue<int, vector<int>, cmp> p;

其他容器

string:字符串。其实不是容器,但是支持许多容器操作。

如:append(string)追加操作、substr(string)子串操作、compare(string)比较操作


容器方法

构造函数

参数情况:无参数;int size;int size, T 元素;同类容器引用;begin, end迭代器。

增加函数

void push_back(const T& x);
void push_front(const T& x);
void emplace_back(const T& x);
iterator insert(iterator it,const T& x);
iterator insert(iterator it,const_iterator first,const_iterator last);//将后两个指针指向的范围的值赋值给前一个指针

删除函数

void pop_back();
void pop_front();
iterator erase(iterator it);
iterator erase(iterator first,iterator last);
void clear();//清空

查找和遍历

T& at(int pos);//需要支持下标的随机访问
T& front();
T& back();
iterator begin();
iterator end();
reverse_iterator rbegin();
reverse_iterator rend();
iterator find(T& x);//查找对应值,没找到返回end()

元素数量

bool empty();
int size();
int count(const_iterator first, const_iterator last, T x);//计算x数量

修改元素

replace(const_iterator first, const_iterator last, T old, T new);//在特定范围内进行替换
void assign(int num, T x);

其他常用操作

void sort(const_iterator first, const_iterator last);
void reverse();//倒序排列
int accumulate(iterator first,iterator last, int sum);//累加
iterator unique(iterator first,iterator last);//去重,实现方法是将后面的元素赋值给前面,尾部的元素仍然保留

其他笔记

如何在main()函数之前执行一条语句?

利用全局变量和构造函数的特性,通过全局变量的构造函数执行。

在main函数前面定义一个全局变量,全局变量会在main函数执行之前初始化,初始化的时候会自动执行构造函数。

如何在main()函数之后执行一条语句?

同理,全局对象的析构函数会在main函数之后执行。

单例模式

详见本博客中《设计模式》一文。

class CSingleton {
// 其它成员
public:
	//提供一个public的静态函数,用这个函数获取实例
	static CSingleton * GetInstance() {
		if (m_pInstance == NULL)//如果没有实例化,进行实例化
			m_pInstance = new CSingleton();
		return m_pInstance;
	}
private:
	CSingleton(){};//将构造函数标记为private
	static CSingleton * m_pInstance;//提供一个成员指针
}

new/delete 与 malloc/free

malloc/free

malloc/free为C的标准库函数

void* malloc(size_t size)//参数代表字节个数

void free(void* pointer)//参数代表内存地址

  • malloc 开辟空间类型大小需手动计算

  • malloc 返回类型是 void*,必须强制类型转换对应类型指针

  • malloc/free 函数只是开辟空间并释放,不会调用构造和析构函数

new/delete

new

  • 调用malloc分配内存空间指针
  • 将指针强制转换为对应类型的指针
  • 调用构造函数

delete

  • 调用析构函数
  • 调用free释放内存

构造函数的初始化列表

对于一般的基本类型成员变量来说,使用初始化列表和在函数体里面赋值并没有区别,但下列三种情况有所差异:

  • 常量成员变量必须用初始化列表,因为无法在函数体里赋值。

  • 引用类型成员变量必须使用初始化列表,因为无法在函数体里赋值(引用本质上是指针常量)。

  • 类类型成员变量,如果在函数体中赋值,需要先调用默认构造函数产生一个空的类实例,再通过拷贝构造函数传给成员变量;而使用初始化列表,可以跳过默认构造函数这一步。此外,没有默认构造函数的类必须使用初始化列表。

成员是按照他们在类中定义的顺序进行初始化的,而不是按照在初始化列表中的顺序初始化。这就要求,如果要使用一个成员变量给另一个成员变量初始化,就会出现问题。

class A {
public:
	int i ;int j;
	A(int x):j(x), i(j){}// 此处i值会先于j值初始化,此时j未定义,导致i未定义
};

一些好的编程习惯

逻辑:

  • 用于被继承的基类的成员函数应当全部声明为虚函数

效率:

  • 使用初始化列表,而不是在函数体内进行赋值
  • 类类型参数传递尽量以常量引用(const &)的方式进行。如果是长度较短的基本类型则使用值传递。
  • 类类型的返回值尽量以引用传递的方式进行

安全:

  • 带有指针的类必须定义拷贝构造函数、赋值拷贝函数和析构函数
  • 将所有的参数设定成private,同时提供setter和getter
  • 用const修饰不会修改成员变量的函数:常量对象只能作为常函数的参数
  • 用const修饰不希望被修改的指针/引用传递参数

其他

  • 使用namespace划分测试程序,便于起变量名

评论区