【C++ 系列笔记】02 C++ 面向对象基础
Content
C++ 面向对象基础
面向对象基础
class Type {
private:
/* data */
public:
Type(/* args */);
~Type();
protected:
};
Type::Type(/* args */) {}
Type::~Type() {}
- 权限
- private 仅内部访问
- public 均可访问
- protected 内部和子类访问
构造函数和析构函数
C++ 默认提供三个函数,无参构造、拷贝构造、析构
我们实任意现一个构造函数,无参构造就不会再提供。
构造函数必须写在 public 下才能被调用到
...
public:
Type(/* args */);
~Type();
...
匿名对象
Type(/* args */);
// 没有引用则会被立即释放
拷贝构造函数
Type(const Type& obj);
-
被调用的条件
通常的方式
classNmae obj2(obj1);
匿名对象
// 使用匿名对象 Type obj = Type(/* args */);
隐式转换(匿名函数的另一种形式)
// 一种隐式转换 Type obj2 = obj1; // 相当于调用了 Type obj2 = Type obj2(obj1); // 类似的 Type obj2 = 1; // 相当于滴啊用了 Type obj2 = calssName obj2(1) // 说白了,1 与 Type 不是一种类型,编译器会去寻找匹配的构造函数,来使得 1 通过构造隐式转换为 Type 类型
函数调用,对象的值传递
void fun(Type param); int main(){ Type obj; fun(obj); // 传值时会实例化一个新对象 }
以值的方式返回对象
void fun(Type param){ // ... return param; // 返回时会实例化一个新对象 } int main(){ Type obj; fun(obj); }
-
注意
-
二义性
Type obj1(); int main(){ Type obj1(); // 这里不会实例化该类,编译器会认为这是一个函数声明 }
-
不能用拷贝构造初始化匿名对象
匿名对象作为左值时,无法通过拷贝构造函数初始化
Type obj(/* args */); Type(obj); // 错误,编译器认为第二行等价于 Type obj;
当作为右值时可以,即上方的使用匿名对象调用拷贝构造
-
深拷贝和浅拷贝
浅拷贝仅拷贝栈区的数据,某些数据是指针,其指向堆区中的数据不变。
深拷贝则会重新申请堆区空间。
Class1(const Class1& obj){
mClass2Obj = new Class2(obj.class2Obj);
}
初始化列表
Type(name, age): mName(name), mAge(age){
// ...
}
类对象作为成员(组合)
构造顺序:先对成员进行构造,然后对本类对象进行构造。
析构顺序:完全相反。
-
构造有参对象成员的方法
class Person(){ Head head; public: Person(headParam): head(headParam){ // ... } };
explicit 关键字
用来设定构造函数的 显式 特性,防止隐式转换。
explicit Type(){
// ...
}
此时就不能进行上述的这种操作了。
Type obj = 1;
C++ 的动态内存分配(动态创建对象)
运算符:new
delete
Type* obj = new Type;
delete obj;
注意不要这样写:
void* p = new Type;
delete p;
// 释放失败
-
new 开辟数组
Type* objArr = new Type[10];
注意,开辟数组会调用类的默认无参构造,所以想要创建对象数组,必须提供无参构造。
不过在栈区的数组可以指定有参构造如:
Type objArr[10] = {Type(/* args /), Type(/ args */), …}
- delete 释放数组
```cpp
delete [] objArr;
常量对象调用方法
const Type&
类型的对象不允许调用未使用const
修饰的方法。
class Type{
private:
int data;
int getData() const{
// 不允许修改成员,但 const Data& 类型的实例允许调用该函数
return data;
}
int setData(int value) {
// 允许修改数据,但 const Data& 类型的实例禁止调用该函数
return data = value;
}
};
void failure(const Data& obj) {
// 报错
obj.setData(1);
}
void success(const Data& obj) {
// 通过
obj.getData(1);
}
int main(){
Type obj;
success(obj);
failure(obj);
return 0;
}
静态成员
-
变量
class Type(){ public: static int foo; // 类内声明 }; int Type::foo = 1; // 类外初始化
静态成员变量在类内声明,在类外初始化,在编译阶段分配内存。
-
方法
class Type(){ public: static int foo(); }; int Type::foo(){ // 实现 }
静态成员方法不可以访问实例的成员,只能访问静态成员变量。
-
静态成员实现单例模式
- 重载构造为私有
- 内部维护一个私有静态的单一实例,并通过公有静态方法提供出去。
示例
#include <iostream> using namespace std; class Foo { private: // 唯一实例的引用 static Foo& foo; // 重载构造函数,使外部无法访问 Foo() { cout << "init" << endl; } Foo(const Foo& foo); public: // 提供该单一实例 static Foo& getInstance() { return foo; } // 其他方法 void method() { cout << "method" << endl; } }; // new 这个单一实例 Foo& Foo::foo = *(new Foo()); int main() { Foo& foo = Foo::getInstance(); foo.method(); system("pause"); return EXIT_SUCCESS; }
类和对象的内存结构
内存结构
-
空类的数据类型大小为 1 字节,这一字节用于为其实例分配内存,没有实际意义。当类中存在成员时,该字节会消失。
-
类的大小不包含成员函数,成员函数在类外。
-
问题:**函数在类外,对象中不包含方法指针,那么方法是如何被对象调用的?****
方法类似全局函数,可以直接 call,其第一个参数是 this。
-
-
类数据类型的大小仅包含成员变量。(还要考虑内存对齐)
- 扩展:
#pragma pack(1)
可以使得编译器以 1 字节对齐内存。
- 扩展:
this 指针
obj.method()
相当于method(obj)
,成员方法默认有一个隐藏的参数 Type* this
==静态成员函数没有 this
指针
method(Type* const this);
-
实现链式编程
Type& method(Type* const this){ // ... return *this; }
-
this 为空
下面这段代码是合法的
Type* p = NULL; p->method();
一个空对象也可以调用他的方法,只不过传入的 this 也为 NULL。
所以很多时候可以看到这样的成员函数定义。
void method(){ if(this == NULL) return; this->attr = ...; }
-
this 的常量修饰(成员函数的修饰)
this 的类型默认为
Type* cosnt
,不允许修改指向注意
const int*
与int const*
相同,均表示 常量数据的指针,即该指针引用的数据不允许更改。int* const
则不同,它表示指针常量,即 指针本身是常量*,不允许修改指向。要使方法内不允许通过 this 修改成员,则应用
const Type* const
来修饰 this应使用如下方法,声明成员函数为 常函数:
void method() const{ // ... }
-
mutable 关键字
用 mutable 修饰的成员变量允许在常函数中被修改。
class Type { private: mutable int data; public: void setData(int value) const{ this->data = value; // 这是合法的 } };
-
友元
-
友元函数
当一个函数声明为一个类的友元,那么它就会被允许访问该类实例的私有成员
class Type { friend void friendFun(Type& obj); // 友元声明 private: int data; }; void friendFun(Type& obj){ obj.data = // ... // 合法访问私有成员 }
类成员函数也可作友元函数,不过需要注意加上作用域
class TypeA { friend void TypeB::friendMethod(TypeA& obj); // 友元声明,注意加作用域 private: int data; };
-
友元类
当一个累声明为另一个类的友元,那么它就会被允许访问这个类的私有成员
注意,类的友元是 单向的,且**无传递性**
class TypeA { friend class TypeB; // 友元声明 private: int data; }; class TypeB { public: void visitA(TypeA& obj){ obj.data = //... // 合法访问私有成员 } };
运算符重载
-
全局函数重载
int operator+(Type& a, Type& b) { return a.data + b.data; }
-
成员函数重载
int Type::operator+(Type& a) { return this->data + a.data; }
-
重载任意类型
int Type::operator+(Type& a, int b) { return this->data + b; }
注意,基本数据类型的运算符不可重载。
-
<< 左移运算符重载
通常情况下,重载
<<
是为了实现cout << obj;
这种情况下,不会将其作为成员函数重载,因为成员函数固定了第一个参数为 this 指针。
调用时是这样的
obj << // sth.
,与 iostream 不相符,所以我们会使用 全局友元函数 去重载。ostream Type::operator<<(ostream& cout, Type& a) { return cout << a.data; }
-
++ – 递增递减运算符重载(以成员函数为例)
-
前置(效率稍微高一些)
Type& operator++(){ this->data++; return &this; }
-
后置
Type operator++(int){ Type temp = *this; // 拷贝一份原状态的对象 this->data++; return temp;
-
}
```
-
* -> 指针、取址运算符重载
Type* operator->(){ return // ... }
Type& operator*(){ return // ... }
-
智能指针
自动释放内存
class SmartPointer{ private: int* pointer; public: // 构造时托管一个指向堆中数据的指针 SmartPointer(int* pointer){ this->pointer = pointer; } // 智能指针对象应开辟在栈上,当对象析构时顺便释放在堆上的数据 ~SmartPointer(){ if(this->pointer){ delete pointer; this->pointer = nullptr; } } // 重载 SmartPointer* operator->(){ return this->pointer; } SmartPointer& operator*(){ return *this->pointer; } };
-
-
*=====* 赋值运算符重载(复杂数据类型自然 地带有 一个浅拷贝的赋值运算符实现)
以成员函数为例
Type& operator=(Type& a){ this->obj = new Foo(a.obj); this->data = a.data; return a; }
-
[] 中括号运算符重载
以 Arr 类的成员函数为例
elementType& operator[](int index){ return this->arrPointer[idnex]; }
-
关系运算符重载
-
== 相等
bool operator==(Type& a){ reeturn this->data == a.data; }
-
!= 不等
bool operator!=(Type& a){ reeturn this->data != a.data; }
-
-
( ) 函数调用运算符
注意,该运算符只能作为成员函数重载
void operator()(){ // ... }
-
注意,不要重载 && 和 ||
重载后会使得其短路特性消失,此时便无法保证表达式内部操作的逻辑性。
因为函数调用总会也必会对所有参数进行求值,这个过程可能导致被运算内容本身的改变,从而出现无法预知的异常。
-
总结
-
= [] () ->
一般通过成员函数重载
-
<< >>
一般通过全局函数配合友元重载
-