结构体和类的区别
C++ 自 1985 年以来就没有结构体,它只有类。使用关键字 class 和关键字 struct 声明的类型都是类。关键字 struct 以及使用该关键字定义类时默认的可见性规则,只是为了与 C 向后兼容而保留,但这是语法问题。 它不会使结果类型实际上是不同的类型。
从两个关键字生成的东西来说,没有区别。特性上的区别主要是默认的权限以及继承权限。
但是大多数程序员会把 struct 使用在纯数据上,没有过多封装。而将 class 使用在封装良好,接口完备,功能强大的地方。
函数中类参数的传递是通过值传递的方式。
声明的内存
struct FHello {
private:
int a = 1;
};
思考一下 FHello 需要存储吗?计算机中虽然所有东西都是二进制,但是分为两种,指令和数据。显然 FHello Hello; 这行代码将会翻译为指令,类型信息就会被这条指令带上。所以 FHello 这个类型并没有在内存中存储起来,而是被当成一个指令来对内存进行操作。
Hello 这个结构体才会在内存中存储起来。FHello 的信息会在程序的代码区中体现。堆栈中所有东西都是无格式的二进制,只有带数据类型信息的指令操作它们时数据才会有意义。
权限
我们将 struct 里的变量叫数据,class 内的变量叫成员。对结构体的的数据进行公共或者私有权限的设置,结构体内的数据默认是 public 权限。而类的变量默认是 private 权限(与 C 向后兼容)。
C++ 通过 public、protected、private 三个关键字来控制成员变量和成员函数的访问权限(也称为可见性),分别表示:公有的、受保护的、私有的。所谓访问权限,就是能不能使用该类中的成员。
#include <iostream>
using namespace std;
struct FHello {
private:
int a = 1;
};
int main() {
FHello Hello;
return 0;
}
public:可以被该类中的函数、子类的函数、友元函数访问,也可以由该类的对象访问;
protected:可以被该类中的函数、子类的函数、友元函数访问,但不可以由该类的对象访问;
private:可以被该类中的函数、友元函数访问,但不可以由子类的函数、该类的对象、访问。
构造和析构
创建一个对象也一样:你得到了一块内存;这块内存可能是“二手房”,前任留下的shit什么的都还留在里面,你得先清理(把内容置零)、重新装修(设置一些基础信息)之后才能入住。
过去,C时代,这些都得你自己照应。如果你忘了,那么访问了未初始化存储区、读出乱七八糟的东西,你就自认倒霉吧。
C++时代,人们变聪明了:既然装修是入住前的必要步骤,我干脆把它固定到你的《购房流程指导书》里算了。你交钱买房后,就会有人领你看房、给你谈装修事宜。
这个固定的、执行装修事宜的步骤就是构造函数。
用伪码表示的话,对象创建流程是这样的:
用各种奇怪的方式得到一块内存
执行构造函数,“装修”这块内存
拎包入住
C++做了一个约定:和类名相同的无返回函数就是它的初始化函数(构造函数),编译器保证在创建一个对象之后、允许你使用它之前,它必定会在这个对象对应的内存上执行构造函数,按你的要求把对象装修好。如果你不写,那么它默认给你个毛坯房(这就是所谓的“默认构造函数”)。
构造函数不为对象的创建分配内存,c++中对普通变量的创建是编译器先为它分配内存(定义),在自动生成一些代码进行初始化,而变量是对象时,需要初始化的代码过于复杂,就需要构造函数来初始化对象。
struct FHello {
FHello() {
}
~FHello() {
}
};
在类里使用构造和析构时记得将它们放在 public 权限下,因为由类创建出来的对象,在调用构造时,会发现没有权限。
析构函数的调用时机在对象出栈的时候进行调用,构造函数可以在新建对象时传入参数。
构造函数可以没有参数但是不可以 FHello fhello();
这样会被编译器认为是一个函数定义,应该是 FHello fhello;
即可
类中的静态变量
类中的静态变量可以通过类名或者实例化出来的对象来访问。静态变量实质上就是一个全局变量,当我们声明一个对象时,并不产生静态变量的拷贝,而是该类所有的实例变量共同一个静态变量。
#include <iostream>
using namespace std;
struct FHello {
static void hello() {
cout << "213" << endl;
};
static const int count = 10;
};
int main() {
FHello Hello;
Hello.hello();
cout << Hello.count << end
}
而类的非静态成员变量,则必须需要实例化出一个对象来调用。
内联函数和私有属性
#include <iostream>
using namespace std;
struct FHello {
private:
int p;
public:
inline int getP() { return p; }
};
int main() {
FHello Hello;
cout << Hello.getP() << endl;
}
内联函数不需要中断调用,直接将要执行的函数贴在当前的函数调用的位置上,通过复制代码节省了函数调用的时间。如果内联函数的函数体太复杂,编译器可能不会将其设置为内联。
宏是预编译器的输入,然后宏展开之后的结果会送去编译器做语法分析。宏与函数等处于不同的级别,操作不同的实体。
内联函数和宏最大的区别在于,内联函数是可以使用函数定义时所在的上下文的。例如上面代码,getP 可以获取到对象的 private p 属性值。如果是宏替换不可以达到这样的效果。如果你的类需要暴露一些私有属性出去,宏显然做不到,那么就用内联函数。inline 弥补了语言一点不足,而宏是对语言的编程。
struct FHello {
private:
int p;
public:
inline int getP();
};
int FHello::getP() {
return p;
}
单继承
class Workers {
public:
void work() {};
};
class ComponentsWorkers :public Workers {
};
int main() {
ComponentsWorkers work;
work.work();
}
称为 Workers 派生出 ComponentsWorkers ,ComponentsWorkers 继承了 Workers 。Workers 称为基类。继承也分继承方式,public 继承将基类除了 private 的成员权限原封不动的继承过来。protected 继承将基类的 public 成员变为 protected 权限。private 继承同理,将基类的 protected 和 public 变成它的 private。
多继承
class ComponentsWorkers :public Workers,public Workers2 {
public:
int fun() {
return a;
}
};
多态主要是为了代码分块。
继承的内存模型
基类和派生类的指针转换,用于处理被覆盖的成员函数。
#include <iostream>
using namespace std;
class Workers {
public:
int a;
void work() {
cout << 1 << endl;
};
};
class ComponentsWorkers :public Workers {
public:
int fun() {
return a;
}
void work() {
cout << 3 << endl;
};
};
int main() {
ComponentsWorkers work3;
work3.work();
(*((Workers*)(&work3))).work();
}
单继承的情况下,继承就是在父类的后面继续添加子类的成员,子类对象=父类对象+其他七七八八的子类成员,所以在用指针进行类型转换的时候,因为sizeof父类一定小于等于子类,这个时候只要直接把子类的指针当成父类的指针来看,就完成了最简单的类型转换。
但是对象在内存中本身没有发生变化,子类再转回父类的时候,对象还是那个子类的对象,所以直接把父类的指针当作子类的指针就完成了转换,最后子类的成员当然还原封不动的在那里。
当然在有虚函数或多继承的时候,或者不保证要转换的对象的实际类型是要转换的类型的时候,直接用指针进行转换类型就不好使了,要用dynamic_cast。
菱形继承和虚继承
//间接基类A
class A{
protected:
int m_a;
};
//直接基类B
class B: public A{
protected:
int m_b;
};
//直接基类C
class C: public A{
protected:
int m_c;
};
//派生类D
class D: public B, public C{
public:
void seta(int a){ m_a = a; } //命名冲突
void setb(int b){ m_b = b; } //正确
void setc(int c){ m_c = c; } //正确
void setd(int d){ m_d = d; } //正确
private:
int m_d;
};
int main(){
D d;
return 0;
}
这段代码实现了的菱形继承,第 22 行代码试图直接访问成员变量 m_a,结果发生了错误,因为类 B 和类 C 中都有成员变量 m_a(从 A 类继承而来),编译器不知道选用哪一个,所以产生了歧义。
可以使用 void seta(int a) { B::m_a = a; }
来指明它具体来自哪个类,也可以使用虚继承来彻底解决多继承时的命名冲突和冗余数据问题。
class B: virtual public A{ //虚继承
protected:
int m_b;
};
//直接基类C
class C: virtual public A{ //虚继承
protected:
int m_c;
};
虚继承的目的是让某个类做出声明,承诺愿意共享它的基类。其中,这个被共享的基类就称为虚基类(Virtual Base Class)。B 和 C 将它们的基类 A 声明为虚基类。
在这种机制下,不论虚基类在继承体系中出现了多少次,在派生类中都只包含一份虚基类的成员。虚派生只影响从指定了虚基类的派生类中进一步派生出来的类(即 D ),它不会影响派生类(B ,C)本身。
友元
class A {
friend class B;
private:
void hello() {}
};
class B {
public:
void init() {
a.hello();
}
private:
A a;
};
在 A 中将 B 声明为友元,这样在 B 中就可以随意访问 A 中的成员。
class A1 {
public:
void SetXXX(int x) { xxx = x; }
int GetXXX() const { return xxx; }
private:
int xxx;
};
class A2 {
friend class B;
private:
int xxx;
};
A1 是常规写法,JAVA 中经常可以看见,建立几个私有然后通过 get set 函数读取和设置,如果仅有几个类使用了这些函数,代码会非常臃肿,友元很好地解决了痛点。
友元维护了封装。友元是库作者自己为了编写方便而使用的,对于使用者来说,某个类有哪些友元是无法被使用者修改的(因为友元声明是在类里面的),使用者依然仅能使用库编写者主动暴露的接口。友元的好处是让编写者自己不用被自己本来要提供给使用者的封装束缚,明明库都是自己写的,内部是自己实现的,这个时候还把自己写的东西对自己封装来降低灵活度,就很不合理了,这个时候才使用友元来简化。
是否使用友元看个人的编程理念或者公司的编程规范。
友元函数
友元函数时可以直接访问类的私有成员或保护成员,它是定义在类外的普通函数,它不属于任何类,但需要在类的定义中加以声明。
class C {
friend void print_t(C& T) {
cout << T.a << endl;
}
private:
int a = 1;
};
int main() {
C c;
print_t(c);
return 0;
}
友元函数破坏了类的封装性和隐藏性,使得非成员函数也可以访问类的私有成员,但维护了整个库的封装性。友元函数和类中的静态函数有点像,静态函数也可以访问类中成员,但是调用静态函数需要加上作用域运算符。友元函数是无法被继承的。
初始化列表
区分初始化和赋值的区别,初始化是创建内存时填入的值,赋值是初始化结束后的赋值。所以列表初始化比直接用 = 赋值更有效率以及时机更加靠前。列表初始化可以初始化 const 类成员,而在函数体内用 = 赋值不可以。
class foo
{
const int i ;
int j ;
foo(int x):i(x),j(i) {
//
};
};
成员是按照他们在类中出现的顺序进行初始化的,这也是叫做列表初始化的原因。
#include <iostream>
using namespace std;
class Base {
public:
Base(int val)
{
m_num = 0;
cout << "create Base(int val)" << endl;
}
private:
int m_num;
};
class BaseChild: public Base {
public:
BaseChild():Base(1) {
cout << "create is BaseChild()" << endl;
}
private:
int m_num;
};
int main(int argc, char *argv[])
{
BaseChild child;
}
在C++中,当创建一个对象时,编译器要保证调用了所有子对象的构造函数,在 Base 中没有定义默认构造函数,只定义了一个有整型参数的构造函数,因此编译器并不会再去生成一个默认的构造函数,而 BaseChild 继承Base 时,又没有显式地指定 Base 的构造函数,所以编译报错。
通过初始化列表显式的调用Base带参构造函数呢。
当一个类没有写构造函数时,会生成一个默认构造函数,这个默认构造函数不会给实例内的基本类型初始化,但是对非初始类型会它的构造函数。
函数默认参数
int a() {}
int a(int b = 0, int c = 0) {}
上面这两种重载会冲突
拷贝
可以直接用 = 来赋值,因为对象是内存对齐的,这种方法拷贝出来的是浅拷贝。
#include <iostream>
using namespace std;
class D{
public:
int m_d;
};
int main() {
D d1, d2;
d1.m_d = 1;
d2 = d1;
cout << d2.m_d << endl;
return 0;
}
编译器除了类增加一个默认构造函数外,也会给类增加一个拷贝构造函数。这样会对 d1 浅拷贝至 d2,如果类中有指针成员,想要实现深拷贝那么就需要自己实现构造函数来拷贝。
#include <iostream>
using namespace std;
class D{
public:
int m_d;
};
int main() {
D d1;
d1.m_d = 1;
D d2(d1);
cout << d2.m_d << endl;
return 0;
}
拷贝构造函数
class D {
D(const D &d) : m_d(d.m_d) {
}
public:
int m_d;
}