【C++ 系列笔记】02 C++ 面向对象基础

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()(){
    	// ...
    }
    
  • 注意,不要重载 && 和 ||

    重载后会使得其短路特性消失,此时便无法保证表达式内部操作的逻辑性。

    因为函数调用总会也必会对所有参数进行求值,这个过程可能导致被运算内容本身的改变,从而出现无法预知的异常。

  • 总结

    • = [] () ->

      一般通过成员函数重载

    • << >>

      一般通过全局函数配合友元重载

原文地址:https://www.cnblogs.com/gaolihai/p/13149747.html