在C++中可以取得内存地址的、有名字的变量就是左值,左值在使用时可以放在等号左侧;右值则是指不能取得内存地址的、没有名字的变量,只能在等号的右侧使用。右值直观上就是临时变量,如执行 a = b + c
时,等号右侧优先计算,加和结果会被保存在临时变量中,这个临时变量就是右值。
在C++中,使用左值的方式,可以是通过其“本名”访问,也可以通过引用访问,此时使用的引用是左值引用。但是右值没有关联名字,为了能够使用右值,C++引入了右值引用的概念。
void func(string& str); // 这是一个左值引用
void func(string&& str); // 这是一个右值引用
问题是我们为什么要直接使用右值?可以看下面这个例子
struct Node {
int val;
Node(const int& _val) : val(_val) {}
};
int input = 10;
Node node_a(input);
Node node_b(10);
上述例子中,两个实例的构造过程基本相同;区别在于 node_a
中传入的是左值引用,在使用完后 input
变量仍然存活,而 node_b
中传入的是一个临时变量,临时变量的值会被复制到 val
中,临时变量使用完后就被销毁了。如果传入的临时变量空间占用非常大,复制过程就是一笔很大的开销,何不保留临时变量并直接将 val
指向临时变量呢?
C++中提供了 move
语义,可以将“将亡值”(即将被销毁的临时变量)有效期的有效期延长,直接保留临时变量而避免上述的拷贝过程。
struct Node {
int val;
Node(const int& _val) : val(_val) {}
Node(int&& _val) { val = std::move(_val); }
};
int input = 10;
Node node_a(input);
Node node_b(10);
a = std::move(b)
可以理解为将 b
这个名字擦掉,以 a
这个名字替换,此时将再也无法使用 b
访问该变量。
C++ 提供了 new 和 delete 关键字来分配和释放内存(堆)
// 或者直接用 auto
string* p_str = new string{"hello, world"}; // 这种花括号的使用也是可以的
delete p_str;
智能指针是C++为了能够更安全地使用动态内存而产生的一种指针,与传统指针不同的地方,智能指针在不使用时会自动释放所指向的对象,避免内存空间浪费。
智能指针使用模板创建:
share_ptr<int> p_int = make_shared<int>(10); // 无须手动释放
关键字 struct 与 class 搜可以由于类的创建,struct 中的所有成员都是公开的,而 class 中的所有成员权限默认是私有的。
默认构造函数
默认构造函数没有参数,会对类的成员进行默认初始化:如果成员变量有默认初始值,则使用默认初始值,如果成员有默认初始化的方法,则使用这些方法;否则,默认构造函数会报错。
如果没有在类中声明任何构造函数,编译器会自动创建一个默认构造函数,但是如果有声明构造函数,编译器就不会自动生成默认构造函数了。此时如果需要默认构造函数,可以手工指定 classname() = default
。
普通的构造函数
使用带有参数的构造函数,为类的成员变量赋初始值,以及进行一些其它的初始化动作。这里可以使用初始值列表:
classname(const type& _a, const type& _b) : var_a(_a), var_b(_b), var_c(0) {}
初始值列表即函数参数之后、函数体之前的部分,还可以在这一部分使用默认初始值来初始化成员变量。
对于类内的 const 成员变量,只能使用初始值列表进行初始化。
使用列表初始化构建类的实例时,会使用该初始值列表,按照成员变量的声明顺序进行初始化操作。
委托构造函数
在构造函数中使用同一类的其它构造函数,就是委托构造函数。委托构造函数的形式:
classname(const type& _a) : classname(_a, 0) {}
这个构造函数使用了上述的带有初始值列表的构造函数。
转换构造函数
由于 C++ 提供的隐式转换,使得构造函数能够使用的参数类型范围更大。如果不想要这种隐式类型转换,可以使用 explicit
修饰构造函数,只能使用在声明处。
拷贝构造函数
拷贝构造函数的参数是类本身,用来实现类的拷贝行为。此外,还有拷贝赋值也实现了类的拷贝,是通过重载 =
运算符实现的。
classname(const classname& _c) : var_a(_c.a), var_b(_c.b), var_c(_c.c) {}
classname& operator=(const classname& _c) { return *this; }
classname p1(p2); // 拷贝
classname p1 = p2; // 赋值
如果没有手工定义拷贝构造函数和拷贝赋值函数,编译器会自动合成;如果想显式地要求编译器合成拷贝构造函数和拷贝赋值函数,使用 =default
;如果想禁止类有拷贝的能力,阻止任何的拷贝构造函数和拷贝赋值函数,使用 =delete
。
移动构造函数
移动构造函数与拷贝构造函数很类似,用来实现类的移动行为。同样的,也有移动赋值运算符。
classname(classname&& _c) : /* 这里不写了 */ {}
classname& operator=(classname&& _c) { return *this; }
编译器不会为类合成移动构造函数和移动赋值函数。
析构函数用于销毁实例。如使用 delete p
是会调用 p
的析构函数进行销毁。
友元是用来控制其它的函数或对象,访问本对象的非公开成员的方法。比如使用打印函数将本对象打印到屏幕,本对象中的某些成员变量是私有的,通过对打印函数添加 friend 前缀,使它可以访问这些私有成员变量。
在类中重载运算符可以丰富类的行为,使类更方便使用。如
=
运算符来实现各种赋值的行为。()
使类成为一个函数对象。[]
使类表现地向容器一样可以通过下标访问。重载运算符以符合日常使用的经验为佳。
虚函数为允许基类调用的派生类的函数。在派生类中不一定要重新定义基类的虚函数,但是重新定义后,基类和派生类就会对该函数拥有各自的版本,在调用该函数时动态绑定。借助指针或引用,虚函数能够实现多态的效果。
class base {
virtual type func() {}
}
class deriver : public base {
virtual type func() {}
}
// 此时使用 base 类型的指针定义 deriver 的对象
base* obj = new deriver();
obj.func(); // 此时会使用 deriver 中的 func,而不是 base 中的 func
从上面的例子可以看出,析构函数最好定义成虚函数,否则 obj 可能不能正确地析构。
纯虚函数为没有实现的函数。纯虚函数的目的是要求派生类必须实现这一函数,主要是为了规范派生类的功能。带有纯虚函数的类称为抽象类,因为纯虚函数没有实现,无法进行实例化。
virtual type function(args) = 0; // 纯虚函数
面向对象编程的三个特征是:封装、继承、多态。
对于 C++ 而言,封装的概念很好理解:通过类的抽象,许多细节被隐藏在了类的内部,使类的用户只需要关心接口如何使用即可;以及使用 public
, private
, protected
对类成员的权限控制,使类的使用者不会触及他们不该访问的类成员。
C++ 支持单一继承或多重继承,提供的抽象类的概念,以及使用 public
, private
, protected
进行继承中权限的控制,都是对继承的支持。在继承中比较容易混淆的是普通成员函数、虚函数、纯虚函数,这里以类指针下的三个函数调用说明区别:
class base {
public:
string f() { return "base.f"; } // 普通函数
virtual string g() { return "base.g"; } // 虚函数
virtual string h() = 0; // 纯虚函数
};
class deriver : public base {
public:
string f() { return "deriver.f"; }
virtual string g() { return "derivr.g"; }
string h() { return "deriver.h"; }
};
int main() {
base* pb = new deriver();
deriver* pd = new deriver();
// 普通的成员函数
cout << pb->f() << endl; // -> base.f
cout << pd->f() << endl; // -> deriver.f
// 虚函数
cout << pb->g() << endl; // -> deriver.g
cout << pd->g() << endl; // -> deriver.g
// 纯虚函数
cout << pb->h() << endl; // -> deriver.h
cout << pd->h() << endl; // -> deriver.h
}
从上面的比较可以看出:
上面的比较可以看出,使用普通成员函数时,基类更像是多种类型相同点的抽象,使用中直接使用派生类会比较合适(实现继承)。而使用虚函数和纯虚函数时,基类看上去更像是多种派生类的统一接口,通过使用基类调用接口相同但行为不同的派生类的功能(接口继承)。这样对于同一基类下的派生类,使用基类作为某个函数的参数就能定义出所有派生类的行为,否则就需要每一个派生类单独定义。这种方式是多态的体现。
原文:https://www.cnblogs.com/ixtwuko/p/cc-p06.html