C++ 基础知识整理
Posted baiiu
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++ 基础知识整理相关的知识,希望对你有一定的参考价值。
前言
本文整理C++基础知识,用于开发中日常查阅。
大气的c++程序
- 类构造函数使用列表初始化,提高效率
- 创建类对象时尽量使用列表初始化直接创建,提高效率
- 添加类成员函数时候时刻注意是否是常量成员函数,即添加const
- 参数考虑是不是pass by reference,或者pass by value
- 考虑函数返回值是return by reference,或者return by value
- 头文件内部可以实现短小精悍的方法,即实现内联函数
- 短小的方法考虑添加inline关键字做内联
- 任何一种函数都可以在成员函数里还是全局函数里实现,操作符重载函数也是如此
static关键字
- 静态成员变量
- 所有对象共享同一份数据
- 在编译阶段分配内存
- 静态成员变量需要在类外进行初始化 - 静态成员函数
- 所有对象共享同一个函数
- 静态成员函数只能访问静态成员变量
struct A
A()
cout << "A 构造" << endl;
;
// 头文件内
class Account
public:
// 静态成员变量必须在类外初始化,声明无定义,此处仅仅是声明了,没有实际分配内存
static double m_rate;
static A a;
static void set_rate(const double x)
m_rate = x;
;
// cpp内,使用前必须进行定义
double Account::m_rate = 0.0; // 静态成员需在类外进行初始化
A Account::a; // 定义
int main()
Account::set_rate(5.0);
cout << Account::m_rate << endl;
cout << Account::a.a << endl;
return 0;
引用,漂亮的指针
引用即别名,引用并非对象,它是为一个已经存在的对象所起的另一个名字。
- 引用必须初始化
- 引用在初始化后,不可以改变
- 引用类型必须和绑定的对象的类型一致,但常量引用会丢失精度
- 引用类型的初始值必须是一个变量,常量引用可以引用常量
int a = 1;
int &ra = a; // 引用必须要初始化
int c = 2;
ra = c; // 赋值操作,把c赋值给b,即赋值给a
// &rb = c; 引用在初始化后,不可以改变
// Non-const lvalue reference to type 'int' cannot bind to a temporary of type 'int'
const int e = 10;
// int &re = e; // 引用类型的初始值必须是一个对象
const int &re = e;
double d = 10.0F;
// Non-const lvalue reference to type 'int' cannot bind to a value of unrelated type 'double'
// int &rd = d; // 引用类型必须和绑定的对象的类型一致,添加const后可以
double &rd = d;
const int &rd1 = d; // 正确,Clang-Tidy: Narrowing conversion from 'double' to 'int'
引用做函数参数
函数传参时,可以利用引用的技术,让形参修饰实参,可以简化指针修改实参。
引用做函数返回值
引用是可以作为函数的返回值存在的。函数调用作为左值。
注意:不要返回局部变量引用。
//返回局部变量引用
int& test01()
int a = 10; //局部变量,内存分配在栈内存上,方法执行完后会回收这块内存
return a;
//返回静态变量引用
int& test02()
static int a = 20;
return a;
int main()
//不能返回局部变量的引用
int& ref = test01();
cout << "ref = " << ref << endl;
cout << "ref = " << ref << endl;
//如果函数做左值,那么必须返回引用
int& ref2 = test02();
// 函数调用可以作为左值
test02() = 1000;
return 0;
引用的本质
引用的本质在c++内部实现是一个指针常量,是所有的指针操作编译器都帮我们做了。
//发现是引用,转换为 int* const ref = &a;
void func(int& ref)
ref = 100; // ref是引用,转换为*ref = 100
int main()
int a = 10;
//自动转换为 int* const ref = &a; 指针常量是指针指向不可改,也说明为什么引用不可更改
int& ref = a;
ref = 20; //内部发现ref是引用,自动帮我们转换为: *ref = 20;
func(a);
return 0;
const关键字
默认情况下,const对象仅在文件内有效。
-
编译器在编译过程中会把用到该const变量都替换成对应的值。
添加volatile后,可以禁止编译器对该值进行优化,必须在运行时去读取该const值。 -
默认情况下,const对象仅在文件内有效。
当多个文件中出现同名const变量时,其实等同于在不同文件中分别定义了独立的变量。因为在编译时会替换,所以相当于是独立的变量。 -
可以使用extern关键字修饰,让他可以在多个文件中被共享。
在声明和定义中都添加extern关键字,这样就只需要定义一次,别的文件也能使用该常量了。
// file1.h头文件中声明,用extern表明该const值并非本文件独有,他的定义将在别处出现
extern const int buffer_size;
// file1.cc文件中定义
extern const int buffer_size = 512;
const修饰变量
-
const修饰的变量也称为常量。
使用关键字const对限定变量的值,避免其他操作改变该值,不可修改;
const 数据类型 变量名 = 常量值
-
const对象一旦创建后就不能改变,所以必须对其进行初始化。
const修饰表达式
常量表达式constexpr, const expression,是指值不会改变并且在编译过程就能得到计算结果的表达式。
const int max_file = 20; // 常量表达式
const int limit = max_file + 1; // 常量表达式
const int size = get_size(); // 不是常量表达式,得运行时才能确定
constexpr int max= 20; // 常量表达式
constexpr int limit = max+ 1; // 常量表达式
constexpr int sz = size(); // 只有当size()函数是一个constexpr函数时才是正确的
const修饰指针
也可以用指针指向const常量或非常量。
指向常量的指针不能用于改变其所指对象的值,也只能使用指向常量的指针。
区分指针常量和常量指针,可以从右往左读,看const具体修饰的是谁。
-
底层const,表示指针所指的对象是一个常量
常量指针,const int *p = &PI。指针指向的值不可更改,但指针的指向可以更改。
常量指针必须初始化,一旦初始化不可更改。 -
顶层const,表示指针本身是个常量
指针常量,int *const p2 = &a。指针的指向不可以改,指针指向的值可以改, -
当对象执行拷贝操作时,进行拷贝的对象要具有相同的底层const资格,或者这两个对象必须的数据类型必须能够互相转换。
const double PI = 3.14;
const double *p = &PI; // 正确,必须添加const关键字,即不能通过*p赋值PI,常量指针
int a = 1;
int b = 2;
// 常量指针,指针指向的值不可以改,但指针的指向可以改
const int *p = &a;
// *p = b; 错误,指向的值不可以改
p = &a; // 指针的指向可以改
// 指针常量,指针的指向不可以改,指针指向的值可以改,
int *const p2 = &a;
*p2 = b; // 指针指向的值可以改
// p2 = &b; 错误,指针的指向不可以改
// const修饰指针和常量,都不可以改
const int *const p3 = a;
// *p3 = b;
// p3 = &b;
const修饰引用
可以把引用绑定到const对象上,就像绑定到其它对象上一样,称之为对常量的引用,可以成为常量引用。
当然const也可以修饰引用,即引用非常量。
int a = 1024;
const int &ra = a; // 正确,允许将一个常量引用绑定到普通对象上,但不允许通过ra改变a的值,a的值依然可以变化
const int b = 1024;
const int &rb = b; // 正确,引用及其对应的常量都是常量
ra = 1;// 错误,不能修改常量
rb= 1; // 错误,不能修改常量
int &r2 = b; // 错误,必须是一个常量引用才行
double c = 3.14;
int &rc = c; // 错误,类型不匹配,Non-const lvalue reference to type 'int' cannot bind to a value of unrelated type 'double'
const int &rc1 = c; // 正确,Clang-Tidy: Narrowing conversion from 'double' to 'int'
const修饰函数形参
可以使用const修饰函数入参,防止在函数体内部修改该传递过来的变量
class Person
public:
string name;
mutable int age;
;
void testPerson(const Person *person)
person->age = 1; // 可以修改
// person->name = "aa"; // 不可修改
void testPerson(const Person& person)
person.age = 1; // 可以修改
// person.name = "aa"; // 不可修改
const修饰成员函数
使用const修饰的成员函数,即常量成员函数,const成员函数内部不允许修改非mutable成员变量。
class Person
private:
int age; // 常量成员函数内不允许修改非mutable的值
mutable int number; // 常量成员函数内可以修改mutable的值
public:
int getAge() const
// age = 1; // 错误,不能更改
return age;
int getNumber() const
number = 1;
return number;
;
常量成员函数重载规则
- 如ClassA所示:在设计函数方法时需要考虑是否设计成常成员函数,因为常成员函数可以被常量对象调用,也可以被非常量对象调用;
- 如ClassB所示:当常成员函数和普调成员函数共存时,则常量对象调用常成员函数,非常量对象调用普调成员函数;
class ClassA
public:
ClassA()
void print() const // 添加const
;
class ClassB
public:
void print()
// 函数重载,const也是函数签名的一部分
void print() const
;
int main()
const ClassA a;
// 'this' argument to member function 'print' has type 'const Test', but function is not marked const
a.print();
const ClassB b1;
b1.print();
ClassB b2;
b2.print();
return 0;
操作符重载
- 使用operator关键字,专门用于定义重载运算符的函数,即operator+函数
- 运算符重载不改变原运算符优先级
- 等价于函数调用,即a + b 等价于函数调用a.operator+(b)
- 运算符重载函数也可以是全局函数
自定义complex复数类
均在头文件内实现,所有函数需添加inline关键字。
#ifndef TESTCPP_TESTOPERATOR_TEST_H
#define TESTCPP_TESTOPERATOR_TEST_H
#include <iostream>
using namespace std;
class complex
public:
explicit complex(double real = 0, double imag = 0) : re(real), im(imag)
// 成员函数重载
complex &operator+=(const complex &x);
// 友元函数,可以访问private值
friend complex operator+(const complex &x, const complex &y);
inline double real() const
return re;
inline double imag() const
return im;
inline void display() const
cout << re << "+" << im << "i" << endl;
private:
double re;
double im;
;
inline complex &
complex::operator+=(const complex &x)
this->re += x.re;
this->im += x.im;
return *this;
// 任何一种函数都可以在成员函数里还是全局函数里实现
inline double real(const complex &x)
return x.real();
inline double imag(const complex &x)
return x.imag();
// 共轭复数
inline complex conj(const complex &x)
return complex(real(x), -imag(x));
/*
* 在全局范围内重载
* 在操作符重载时考虑成员函数内重载 or 全局函数重载
* 如下加法重载,放在全局函数
*/
// 加法,两个参数
inline complex operator+(const complex &x, const complex &y)
// 尽量使用列表初始化
// complex result;
// result.re = x.re + y.re;
// result.im = x.im + y.im;
// return result;
return complex(real(x) + real(y), imag(x) + imag(y));
inline complex operator+(const complex &x, double y)
return complex(real(x) + y, imag(x));
inline complex operator+(double x, const complex &y)
return complex(x + real(y), imag(y));
// 正号
inline complex operator+(const complex &x)
return x;
// 负数
inline complex operator-(const complex &x)
return complex(-real(x), -imag(x));
// 相等
inline bool operator==(const complex &x, const complex &y)
return real(x) == real(y) && imag(x) == imag(y);
// 特殊的操作符<< 只能全局函数重载
inline ostream &operator<<(ostream &out, const complex &x)
return out << x.real() << "+" << x.imag() << "i";
#endif //TESTCPP_TESTOPERATOR_TEST_H
模板
模板类
类模板使用时候需要明确指示类型。
template<typename T>
class complex
public:
complex(T r, T i) : re(r), im(i)
T real() const return re;
T imag() const return im;
private:
T re, im;
;
int main
complex<double> a(1.5, 1.5);
complex<int> b(1, 1);
return 0;
模板方法
如下模板方法,任意类型都可以,只要重载了<符号。
template<class T>
inline const T& min(const T& a, const T& b)
return a < b ? a : b;
多态与virtual
-
多态分为两类
- 静态多态: 函数重载 和 运算符重载属于静态多态,复用函数名
- 动态多态: 派生类和虚函数实现运行时多态 -
静态多态和动态多态区别:
- 静态多态的函数地址早绑定 - 编译阶段确定函数地址
- 动态多态的函数地址晚绑定 - 运行阶段确定函数地址 -
动态绑定的三个条件
- 通过指针调用
- 调用的方法是一个虚函数
- 调用者向上转型,即通过父类调用 -
虚析构和纯虚析构
- 虚析构或纯虚析构是用来解决通过父类指针释放子类对象
- 如果子类中没有堆区数据,可以不添加虚析构和纯虚析构
- 纯虚析构需要在类外进行实现
- 拥有纯虚析构的函数也是一个抽象类,不能被实例化
何时添加virtual关键字
在设计类时添加virtual关键字:
- non-virtual函数:不希望派生类重新定义它,即不希望子类overrid它
- virtual函数:希望派生类重新定义它,但是有一个默认的实现
- pure virtual函数: 派生类一定要重新定义它才能被实例化,含义纯虚函数的类是一个抽象类,不能被实例化
虚指针vptr和虚表vtbl
观察者模式
struct Observer
public:
virtual void onNotify() = 0;
;
class Subject
public:
void addObserver(Observer *observer)
m_views.push_back(observer);
void notify() const
for (auto obj : m_views)
obj->onNotify();
private:
vector<Observer *> m_views;
;
// public公有继承
class MyObserver : public Observer
void onNotify() override
cout << "MyObserver onNotify" << endl;
;
void main()
Subject subject;
MyObserver observer;
subject.addObserver(&observer);
subject.notify();
return 0;
Big Three: 拷贝构造、拷贝赋值和析构
-
默认情况下,c++编译器至少给一个类添加4个函数
- 缺省的无参构造函数,A() = default;
- 缺省的拷贝构造函数,对属性进行值拷贝,按位拷贝,A(const A& a) = default;
- 缺省的拷贝赋值函数,对属性进行值拷贝,按位拷贝,A& operator=(A& a) = default;
- 缺省的析构函数 ~A() = default; -
C++中拷贝构造函数调用时机通常有三种情况
- 使用一个已经创建完毕的对象来初始化一个新对象
- 值传递的方式给函数参数传值
- 以值方式返回局部对象 -
重写拷贝赋值函数时需要考虑自拷贝这种情况
深拷贝、浅拷贝
编译期默认的是浅拷贝,按位拷贝,所以如果成员变量为指针类型的话,会导致两个对象指向同一块内存;
自定义class如果内部有成员变量为指针类型的话,则必须重写拷贝构造、拷贝赋值和析构函数,即进行深拷贝。
当要进行深拷贝时,即类成员有指针时,如果不进行深拷贝,就会有双重free问题(pointer being freed was not allocated),所以必须进行深拷贝;
而进行移动时候会将原先的指针置为NULL,所以不会有这种问题,即是浅移动。
左值、右值与右值引用
-
左值 lvalue 是有标识符、可以取地址的表达式,最常见的情况有:
变量、函数或数据成员的名字
返回左值引用的表达式,如 ++x、x = 1、cout << ’ ’
字符串字面量如 “hello world”
在函数调用时,左值可以绑定到左值引用的参数,如 T&。一个常量只能绑定到常左值引用,如 const T&。 -
纯右值 prvalue 是没有标识符、不可以取地址的表达式,一般也称之为临时对象,即临时对象就是一种右值,最常见的情况有:
返回非引用类型的表达式,如 x++、x + 1、make_shared(42)
除字符串字面量之外的字面量,如 42、true -
右值引用
Rvalue references are a new reference type introduced in c++11 that help solve the problem of unnecessary copying and enable perfect forwarding.
When the right-hand side of an assignment is an rvalue, then the left-hand side object can steal resources from the right-hand side object rather than performing a separate allocation, thus enabling move semantics.
右值引用可以解决不必要的copy、并能进行完美转发。可以理解为在进行赋值操作时,左值偷了右值引用的资源,不必要进行额外的分配。 -
左值持久,右值短暂;移动右值,拷贝左值;
-
引入一种额外的引用类型当然增加了语言的复杂性,在类中将会引入移动构造函数和移动拷贝函数,即move constructor 和 move assignment。
-
右值引用和容器类型一起使用时,将会大大提高容器的效率。如vector.insert(T &x),vector.insert(T &&x);
std::move
std::move函数可以以非常简单的方式将左值引用转换为右值引用。
-
C++ 标准库使用比如vector::push_back 等这类函数时,会对参数的对象进行复制,连数据也会复制。
这就会造成对象内存的额外创建, 本来原意是想把参数push_back进去就行了,通过std::move,可以避免不必要的拷贝操作。新版本可以使用emplace_back等函数。 -
std::move是将对象的状态或者所有权从一个对象转移到另一个对象,只是转移,没有内存的搬迁或者内存拷贝所以可以提高利用效率,改善性能。
-
对指针类型的标准库对象并不需要这么做。
-
标准库代码:
template <class _Tp>
inline constexpr
typename remove_reference<_Tp>::type&&
move(_Tp&& __t) noexcept
typedef _LIBCPP_NODEBUG_TYPE typename remo以上是关于C++ 基础知识整理的主要内容,如果未能解决你的问题,请参考以下文章