C++学习笔记——结构体和类

结构体和类

定义

将多个对象放置到一起视为一个整体

//定义
struct str
{
    int x;
    double y;
};

//声明
struct str;
//仅有声明只知道str是一个struct但其内部不知道,此时str是incomplete type
//但可以定义str* mystr; 所有结构体的指针大小都一样64位机8个字节

注意:

  • 结构体是翻译单元级一处定义,可以在不同翻译单元里有相同的定义
  • 结构体数据成员的定义为隐式,在构造出对象时再定义,在struct的定义中视为声明,即便是结构体内部的int x = 3;也是声明

初始化

  • 聚合初始化:str m_str{3,4}; 默认为0(缺省初始化)
  • C++20 指派初始化 str m_str{.x = 3,.y = 4};

mutable限定符

const str m_str; const结构体对象的成员是不可修改的,但可以通过定义时加mutable限定符来开后门

struct str
{
    mutable int a = 0;
    int b = 3;
};
int main()
{
    str a;
    a.x = 1; //ok
    a.b = 1; //wrong
}

静态数据成员

类外定义类内声明,除了const-static对象以外其他静态成员都必须在类外初始化

struct str
{
    static int x; //ok
    static int x = 10; //error
    const static int x = 10; //ok
};
int str::x=10; //类外定义,给出一块内存,所有对象共享这块内存,不定义会出现链接错误,编译时不会出错
//即便是const static int x = 10;也需要在类外定义,此时只知道x的值,但是如果想要获取地址,还需要定义const int::x = 10;
//类外定义指的是函数外定义,主函数也是函数,如果定义在函数里岂不是只有运行这个函数的时候(运行期)才会定义,所以会链接错误

const数据静态成员:

const-static数据成员可以在类内初始化,但需要在外部提供定义,否则只有值无址,而constexpr-static数据成员必须在类内初始化,因为constexpr隐含inline修饰,constexpr-static不要再在类外部定义了,冗余且可能会报错

C++17内联数据静态成员:

struct str
{
    inline static int x = 10;   //内联静态成员必须在类内部初始化
};
//不能再在类外部进行初始化定义了,否则会重定义

或者这样

struct str
{
	static int x;
};

inline int str::x = 10;

成员函数

struct str
{
    int x = 3;
    void fun()
    {
        std::cout<<x;  //结构体内部对象无需显示说明参数
    }
};

成员函数的声明与定义

  • 类内定义会隐式内联,防止多处定义
  • 类内声明类外定义,不会隐式内联,如需内联,需加inline
struct str
{
    int x = 3;
    inline void fun();
};

void str::fun()
{
    std::cout<<x;  //结构体内部对象无需显示说明参数
}

两次编译特性

struct str
{
    void fun()
    {
        x++;
    }
    int x;
}

编译不会报错,编译器为了迎合程序员的需求(重要的写前面),采用两次编译,第一次大致看看结构体的成员都有啥,第二次再细看函数内容

this指针

类型:str* const

所有成员函数中输入有一个隐藏参数this,指向当前对象的地址&str,用于区分成员函数内部成员和形参

struct str
{
    int x;
    void(int x)
    {
        x;//形参x
        this->x;//成员x
    }
};

const成员函数

struct str
{
    void fun() const
    {
        
    }    //使得this变为const str* const类型
};

注: 常量类对象只能调用常量成员函数和静态成员函数,无法调用普通成员函数,故如果是只读的尽量加上const修饰

成员函数的名称寻找与隐藏关系

int x;
struct str
{
    int x;
    void fun(int x)
    {
        
    }
};

inline void str::fun(int x)
{
    x;  //形参x
    ::x;  //全局x
    this->x;  //成员x
    str::X;  //成员x
}

静态成员函数

static void fun()
{
    
}
//无隐藏参数this 内部不能直接用普通的成员
//可用静态的数据成员,也可返回静态的数据成员
str::fun();
str1.fun();

利用静态成员函数构造对象

struct str
{
    static auto& instance()
    {
        static str x;
        return x;
    }
}

成员函数基于引用限定符的重载

struct str
{
    void fun() &; //左值调用
    void fun() &&; //右值调用
    void fun() const &;//常量左值调用
    void fun() const &&;//常量右值调用
};
str a;
a.fun(); //调用void fun() &;
str().fun(); //调用void fun() &&;

访问限定符

struct str
{
private:
    int x;
public:
    int y;
protected:
    int z;
};
  • struct不写限定符默认为public
  • class不写限定符默认为private
  • private指只能在类友元函数或类成员函数中访问这些对象,这个类可以是不同对象
struct str
{
public:
    void fun(str& input)
    {
        input.x;  //ok
    }
private:
    int x;
};
  • 访问限定提供了封装特性,(C++三大特性,封装、多态、继承)
  • 封装:把一些东西封装起来不让别人看到,对外只提供接口来访问。例如电视机,你看不到内部的零件,你只能通过遥控器来控制

友元函数friend

打破访问限制

//声明main是类的友元函数
int main();
class str2;
class str
{
    friend str2;
    friend int main();
    inline static int x;
};

int main()
{
    std::cout<<str::x;
}

class str2
{
    
};

声明较为麻烦可以删除,但不能加限定符

struct str
{
    friend str2; //虽然没见过,但是此处认为是一个声明
	friend void fun();
	//不能加限定符 friend void ::fun(); error 此时不能认为是一个声明
    int x = 10;
};

void fun(str& input)
{
    std::cout<<input.x;
}

class str2
{
    
};

友元函数的类内定义

class str
{
    int y;
    friend void fun()
    {               //虽然是在类内定义,但还是全局域函数
        str val;
        std::cout<<val.x;
    }
};
C++隐藏友元机制,虽有定义,但常规寻找找不到友元函数(可减轻编译器负担),因此无法在全局中直接调用fun(),必须在类外进行声明,或者把定义放在类外。

ADL实参依赖寻找 argument dependent lookup:非限定寻找时,除常规寻找外,还会对参数命名空间中寻找这个函数(只针对自定义类型)

class cls
{
    friend void fun(const str val)
    {
        
    }
};
int main()
{
    str val;
    fun(val);  //会在val的命名空间(cls的定义)中寻找这个函数
}

解决了隐藏友元找不到的问题,这才是隐藏友元正确的打开方式,友元函数的定义就是为了访问私有对象,不引入类对象实参,为什么要用友元函数。

构造函数

class cls
{
public:
    cls(int z):x(z),y(z)  //初始化列表
    {
        
    }
private:
    int x;
    int y;
};

注意:

  • 元素初始化顺序与声明相关,与初始化列表的顺序无关
  • 元素的构造和初始化在初始化列表那一步就完成了,即使不写系统也会默认给出来进行缺省初始化
  • 缺省构造函数cls(),调用时用cls a{},不能cls a()会引发歧义(被编译器误认为是函数声明)
  • 已有构造函数编译器不会再提供默认缺省构造函数,但可以显式给出说明cls() = default

拷贝构造函数

struct str
{
    str() = default;
    
    str(const str& x)  //必须加引用否则会无限循环,尽量再加上const
       :val(x.val){}
       
    int val;
    
};

str m;  //默认缺省构造函数
str m1(m); //拷贝构造函数
str m1 = m; //拷贝构造函数
显式声明默认拷贝构造函数:str(const str&) = default;

移动构造函数

左值右值引用

引用是对象的别名,左值引用是对左值的引用,右值引用是对右值的引用。通过右值引用使得右值的声明周期与右值引用类型变量的生命周期一样。借助右值引用实现移动语义和完美转发。完美转发见模板知识补充最后。

int x; 
int& y = x;  //左值引用
int& z = 3;  //错误,对右值左值引用
const int& z = 3;  //正确 const是个例外,可以对右值左值引用
int&& a = 1;  //右值引用
int&& a = x;  //错误,对左值右值引用

移动语义:

std::string ori("abc");
std::string newstr = std::move(ori);
//move:从输入的变量身上偷取资源,偷完之后newstr为"abc",ori为空
//move针对的是类类型对象的移动,非类类型对象可以用y = std::exchange(x,new_value)对非类型成员显示移动
//exchange在#include <utility>中

移动构造函数

struct str
{
    str(str&& x)
       :a(std::move(x.a)){}
       
    std::string a = "abc";
};

注意:

  • 默认移动构造函数str(str&&) = default;
  • 定义了拷贝构造函数,编译器就不会自动生成移动构造函数
struct str2
{
    str() = default;
    str2(const str2&)
    {
        std::cout<<"copy"<<std::endl;
    }
};

struct str
{
    str() = default;
    str(const str&) = default;
    str(str&&) = default;
    int val = 3;
    std::string a = "abc";
    str2 m_str2;
};

int main()
{
    str m;
    str m2 = std::move(m);
}
  • 进行移动构造时会对所有成员进行移动构造,对于无移动构造的类或结构体对象,会调用其拷贝构造函数,二者都没有就会报错
  • 建议定义移动构造函数时加上noexcept,保证不抛出异常来提高效率
  • 右值引用对象在类或函数体内的表达式中是左值
str(str&& x)
{
    std::string tmp = x.a;
    std::cout<<&x<<std::endl;
}

拷贝赋值函数与移动赋值函数

str m;
str m2 = std::move(m);  //调用移动构造函数
str m3 = m;  //调用拷贝构造函数
m2 = m;  //调用拷贝赋值函数
m2 = std::move(m) //调用移动赋值函数

str& operator = (const str& x)
{
    val = x.val;
    return *this;  //以实现a=b=c连等
}

str& operator = (str&& x)
{
    val = std::move(x.val);
    return *this;
}

析构函数destrutor

~str()
{
    
}  //无参数返回类型,无形参。对象释放时调用

析构函数执行完进行内存回收,显式内存必须显式释放

str* m = new str();
delete m;

析构函数尽量加上noexcept修饰符

使用new开辟空间易出现的问题

class str
{
public:
    str():ptr(new int()){}
    ~str(){delete ptr;}
    int& data()
    {
        return *ptr;
    }
private:
    int* ptr;
};

int main()
{
    str a;
    a.data() = 3;
    str b(a);
    //错误,无拷贝构造函数,系统自动生成,其中包含a.ptr = b.ptr;
    //使得两个指针指向同一块内存,在析构时这块内存被释放了两次,系统崩溃。
}

//改正
//增加拷贝构造函数和拷贝赋值函数
str (const str& val)
    :ptr(new int())
{
    *ptr = *(val.ptr);
}

str& operator = (const str& val)
{
    *ptr = *(val.ptr);
    return *this;
}

delete 关键字

加上之后函数不能被调用(不是未声明)

C++编译器优化

//按常理说
str a1(20); //直接构造
str a1(a2); //直接构造
str a1 = str(2); //拷贝构造
str a1 = a2; //拷贝构造
str a1 = std::move(a2) //移动构造

//但实际上由于编译器的优化行为
str a1(a2); //a2已经存在,所以会使用拷贝构造函数
str a1 = str(2);
str a1{str(2)}; //直接用直接构造来构造对象,不用先构造在移动了
//但不能给移动构造加delete,不然你就会发现这个优化的秘密
//C++17后必须进行优化,加任何编译器参数都不行

运算符重载

  • 不能发明新的运算符,不能改变运算符的优先级与结合性,一般不改变原意
  • 除()外其他运算符不可缺省
auto operator + (str x, int y = 2){}  //错误

class str
{
    auto operator () (int y = 3)
    {
        return val+y;
    }
    int val = 5;
};

int main()
{
    str x;
    x();
    x(3);
}
  • 类内运算符重载默认第一个参数为*this
  • 可重载且必须实现为成员函数的运算符 = [] () -> 转型运算符
  • 可以实现为非成员函数的运算符 + - * / % 等
  • 不建议重载的运算符 && || ,
  • 不可重载的运算符 ? : (三元运算符)等
  • 对称运算符不要定义为成员函数
class str
{
public:
    str(int x):val(x){}
    
    auto operator + (str a)
    {
        return str(val + x.val);
    }
private:
    int val;
};

int main()
{
    str x = 3;
    str y = x + 4;  //x + 4可以 4 + x就不行了
}

在外定义可能会由于private属性导致无法访问一些成员,故将其声明为友元函数,在使用+运算符时由于ADL,故可以使用

struct str
{
    str(int x):val(x){}
    friend auto operator + (str in1, str in2)
    {
        return str(in1.val + in2.val);
    }
private:
    int val;
};
  • 移位运算符一定定义为非成员函数以保证第一个参数为流
friend auto& operator << (std::ostream& str, str input)
{
    ostr<<input.val;
    return ostr;  //用于实现类似于cout<<A<<B;
}
  • operator[]实现读写
int& operator [] (input id)
{
    return val;   //第一个参数为*this
}
//为保证输入对象为const时依然可用引入重载
int operator [] (int id ) const
{
    return val;
}
  • ++ – 运算符的重载
str& operator ++ ()   //前缀自增
{
    ++val;
    return *this;
}

str operator ++ (int x)  //后缀自增
{
    str tmp(*this);   //性能很低,会创建临时对象
    ++val;
    return tmp;
}
  • -> * 运算符的重载
class str
{
public:
    str(int* p):ptr(p){}
    
    str* operator -> ()
    {
        return this;
    }

    int& operator * ()
    {
        return *ptr;
    }
private:
    int* ptr;
    int val = 100;
};
int main()
{
    int x = 10;
    str ptr = &x;
    *ptr; //10
    *ptr = 11;
    ptr -> val;  //被编译器自动翻译为ptr.operator->()->val;
}
  • 类型转换运算符重载
class str
{
public:
    str(int x):val(x){}
    operator int () const  //不用显式给出返回类型
    {
        return val;
    }
private:
    int val;
}
str obj(100);
int v = obj;

以上代码可能会引入歧义 obj + 3; 可以是(int)obj + 3 也可以是 obj + (str)3,为避免歧义需要对构造函数加explicit修饰符或对重载函数加explicit,使用的时候尽量使用显式类型转换

class str
{
public:
    explicit str(int x):val(x){}
    operator int () const  //不用显式给出返回类型
    {
        return val;
    }
private:
    int val;
}
str obj(100);
int v = obj;
  • C++20中对于 == 和 <=>的重载
friend bool operator == (str obj1, str obj2)
{
    return obj1.val == obj1.val;
}

friend bool operator == (str obj1, int x)
{
    return obj1.val == x;
}
obj == 100;
100 == obj;
//C++20中重载==会自动引入!= 但不能重载!=引入==
#include <compare>
auto operator <=> (int x)
{
    return val<=>x;
}
//返回类型strong_ordering,weak_ordering,partial_ordering
//即可用于任意比较,不用在重载别的了

类的继承

graph LR
triangle-->shape
right_triangle-->triangle
rectangle-->shape
square-->rectangle

知识点

  • 描述…是一个…的关系,直角三角形是一个三角形,三角形是一个形状
    *通常用public继承(公有继承)
  • struct默认公有继承
  • private默认是私有继承
  • 公有继承:从基类中继承的公有成员和保护成员访问权限不变
  • 私有继承,从基类中继承的公有成员和保护成员变为为私有
  • protected限定符:为派生类开一个后门,使得其可以被继承但外部依然无法访问
header 1 public继承 protected继承 private继承
public int x public int x protected int x private int x
protected int y protected int y protected int y private int y
private int z 无法继承 无法继承 无法继承
  • 继承不是类的声明,不可class str :public base;
  • 可以用基类指针或引用指向派生类对象,但只能调用或访问基类中被继承的对象
class Base
{
public:
    int x = 10;
    void fun()
    {
        std::cout<<"asd";
    }
};

class Derive : public Base
{
public:
    void fun2()
    {
        std::cout<<"asdasd";
        fun();
    }
};
int main()
{
    Derive a;
    Base& ref = a;
    ref.fun2(); //错误
    ref.fun(); //正确  相当于ref只引用了Derive的Base部分
}
  • Derive是一个Base,故可以访问那些被继承的成员
  • 类的派生会形成派生域,派生类所在域位于基类内部,派生类的名称定义会覆盖基类,可使用域操作符显式访问基类
class Derive : public Base
{
    int x;
    x;
    Base::x;
};
  • 派生类中调用基类构造函数
struct Base
{
    Base(int){}
};

class Derive : public Base
{
    Derive(int a)
        :Base(a)   //若不写则系统会隐式调用缺省构造函数
    {
            
    }
};

虚函数

非静态函数,非构造函数

定义

class Base
{
    public:
    virtual void f(){cout<<"Base::f"<<endl;}
    virtual void g(){cout<<"Base::g"<<endl;}
    virtual void h(){cout<<"Base::h"<<endl;}
};

class Derived:public Base
{
	public:
    virtual void f(){cout<<"Derived::f"<<endl;}
    virtual void g1(){cout<<"Derived::g1"<<endl;}
    virtual void h1(){cout<<"Derived::h1"<<endl;}
};

虚函数表vtable(vitrual table)结构

基类对象存储结构
vtable
其他成员
其他成员

基类vtable中存储的是基类自己定义的虚函数的信息

Base::f() Base::g() Base::h()
派生类对象存储结构
vtable
其他基类的vtable
其他成员

派生类vtable中存储的是基类中虚函数的信息(若虚函数进行了重写则覆盖基类)和自己定义的虚函数的信息

Derive::f() Base::g() Base::h() Derive::g() Derive::h()
class Base
{
public:
	virtual void func()
	{
		cout << "Base!" << endl;
	}
};
class Derived :public Base
{
public:
	virtual void func()
	{
		cout << "Derived!" << endl;
	}
};

void show(Base& b)
{
	b.func();
}
Base base;
Derived derived;

int main()
{
	show(base); //Base!
	show(derived);  //Derived!
	base.func();  //Base!
	derived.func();  //Derived!
	return 0;
}
由动态类型引入的多态,同样调用一个函数,
由于输入的类型不同导致函数表现出不同的行为。
  • 虚函数在基类中定义,对其引入缺省逻辑或干脆直接声明为纯虚函数virtual void fun() = 0;声明基类为抽象基类,在派生类中重写该虚函数(抽象基类的派生类必须重写虚函数)。
class Base
{
public:
    virtual void fun() = 0;    
};
class Derive : public Base
{
public:
    void fun(){}
};
class Derive2 : Derive
{
    //Derive2基类为Derive Derive中有定义故Derive2不是抽象类
};
  • 若声明为纯虚函数则在派生类中必须重写该函数,否则视为抽象类,无法构造对象
  • 抽象类无法构造对象,但可以定义其指针或引用类型绑定到派生类
  • 虚函数重写(override)要求函数范围类型,参数列表完全相同,重写不是重载
  • 重写后的虚函数仍是虚函数,保留特性不变
  • 可以显式声明重写,在函数最后加override修饰符

虚函数注意的点:

  • 由虚函数所引入的动态绑定属于运行期行为
  • 缺省实参只会考虑静态类型
  • 虚函数调用成本高于非虚函数
  • 需要用指针(或引用)引入动态绑定
  • 构造函数里调用虚函数可能会由于派生类虚函数没有构造好,但基类已经构造好,从而调用基类的虚函数
class Base
{
public:
    Base()
    {
        fun();
    }
    virtual void fun(){}
};

class Derive : public Base
{
    Derive()
        :Base()
    {
        fun();  //会先调用Base的fun在调用Derive的fun
    }
    virtual void fun(){}
};
  • final关键字,告诉编译器其所有的派生类虚函数不会再重写,提高性能
void fun() override final
{
    ...
    //表明该函数不会再重写
}

class Derived final: public Base
{
    ...
    //表明该类不会再被继承
};
  • 将析构函数声明为虚函数,则所有派生类的析构也是虚函数,目的:使用基类指针删除派生类对象,此时会引起编译器行为的不确定,但若将析构函数声明为虚函数virtual ~Base() = default;则删除对象时会先析构Derived再析构Base
Derived* d = new Derived();
Base* b = dl
delete b;
//注意是用基类指针删除派生类对象时会引发不确定性,要是用派生类的指针删除派生类对象则一定会先析构派生类再析构基类,不会引发歧义。(先构造的后销毁)
  • 派生类中缺省定义的狗砸函数会隐式调用基类的相应构造函数,派生类的其他构造函数函数(自己定义的构造函数)会隐式调用缺省缺省构造函数(若未显式说明)
class Base
{
public:
    Base(int x)
        :a(x){}
        
    Base(const Base& str1)
        :a(str1.a){}
        
    Base(Base&& str1)
        :a(std::move(str1.a)){}
        
    Base& operator = (const Base& val)
    {
        a = val.a;
        return *this;
    }
    
    int a;
};

class Derived : public Base
{
public:
    Derived(int x)
        :Base(x){}
        
    Derived(const Derived& str1)
        :Base(str1){}
    
    Derived& operator = (const Derived& val)
    {
        Base::operator = (val);  //a = val.a
        return *this;
    }
    
};

使用using改变成员修饰符

header 1 public继承 protected继承 private继承
public int x public int x protected int x private int x
protected int y protected int y protected int y private int y
private int z 无法继承 无法继承 无法继承
struct Base
{
public:
    int x;
private:
    int y;
protected:
    int z;
    void fun(){}
};

struct Derive : public Base
{
public:
    using Base::z;  //protected z -->public
    using Base::fun;  //fun --> public 针对的是fun的所有重载版本
private:
    using Base::x  //public x -->private
};

使用using继承基类构造函数逻辑,适用于派生类并未引入新的数据成员,此时不用重写派生类的构造函数。

struct Base
{
public:
    Base(int x)
        :val(x){}
    int val;
};

struct Derive : public Base
{
public:
    using Base::Base;
};
Derive A(100);

友元函数不能访问基类成员

struct Base
{
    int x = 10;
};

struct Derive : public Base
{
    friend void fun(const Derive& val);
};

void fun(const Derive& val)
{
    val.x;  //error错误 友元无法访问基类成员
}
//且友元关系无法继承

利用基类指针实现在容器中保存不同类型对象

struct Base
{
    virtual double getvalue() = 0;
    virtual ~Base() = default;
};

struct Derive : public Base
{
    Derive(int x)
        :val(x){}
    double getvalue() override
    {
        return val;
    }
    int val;
};

struct Derive2 : public Base
{
    Derive2(double x)
        :val(x){}
    double getvalue() override
    {
        return val;
    }
    double val;
};

int main()
{
    std::vector<std::shared_ptr<Base>> vec;
    vec.emplace_back(new Derive{1});
    vec.emplace_back(new Derive2{13.14});
    std::cout<<vec[0]->getvalue()<<vec[1]->getvalue()<<std::endl;
}
//将析构函数设置为虚函数为了防止delete时出错

多重继承可能存在的问题

struct Base
{
    
};
struct Base1 : public Base
{
    
};
struct Base2 : public Base
{
    
};
struct Derive : public Base1, public Base2
{
    
};
graph LR
Base1-->Base
Base2-->Base
Derive-->Base1
Derive-->Base2

Base里的对象Base1和Base2都有,导致Derive不知道用哪个

引入虚继承

struct Base1 : virtual Base
struct Base2 : virtual Base
struct Derive : public Base1, public Base2
此时编译器只会引入一个共有对象
上一篇:Java描述 LeetCode,501. Find Mode in Binary Search Tree 找出二叉树中的众数 Morris算法 详解


下一篇:STM32(1):点亮LED(上)