类
类的成员函数可以在类外定义,方便代码共享。
例如,要从一个文件中(设为A.cpp)使用另一个文件(设为B.cpp)中定义的类(设为Entity)。
方法一、将该B.cpp中的Entity类复制到A.cpp中,且Entity类的所有成员函数,包括构造函数、析构函数都得在类外定义,Entity类中只有类成员的声明。
这样就与函数的声明与定义分离一样,在A.cpp中使用Entity类成员函数,编译器在本文件中识别到该函数的声明,然后在链接阶段到其他文件中寻找函数定义。
补充:
//A.cpp
class Entity
{
int a;
public:
void sayHello();
void getA()
{
cout << a << endl;
}
};
void Entity::sayHello()
{
cout << "Hello" << endl;
}
//B.cpp
class Entity
{
int b;
public:
void sayHello();
void getB()
{
cout << b << endl;
}
};
在B.cpp中调用sayHello()成员函数会调用A.cpp文件中的定义的同名函数,但在B.cpp中实例化的Entity类没有成员变量a和成员函数getA()。
在B.cpp中定义sayHello()成员函数会链接错误“函数重定义”。
方法二、将类的声明写在.h文件中,成员函数的定义写在同名的cpp中。要使用该类时将头文件include。
类的内存分配
(来源:https://www.cnblogs.com/rednodel/p/9300729.html)
一个空类class A{};占用一个字节,是为了区分空对象占内存的位置。一旦类有了成员变量后,调用sizeof获得的类的空间大小就等于类的所有非静态成员变量大小的总和。类的成员函数存放在代码区。
类对象实例化时在堆区开辟的内存只用于存放非静态成员变量or常量。所谓的“某对象的函数”,如Entity e; e.func();只是从逻辑角度而言,调用了“对象e的成员函数func()”。实际上所有Entity类生成的实例都“共享”func()成员函数。成员函数不论在类内外定义,或者被声明为inline函数都遵循这一规则。
静态/非静态成员函数都存在代码区,但非静态成员函数不能直接调用,是因为非静态成员函数内置了一个指向类对象的指针型参数(*this),非静态成员变量用该指针区分不同的对象。
class Entity
{
int a;
public:
void f1()
{
cout << "e.f1()"<< endl;
}
static void f2()
{
cout << "e.f2()" << endl;
}
};
int main()
{
Entity *e = nullptr;
e->f1();
e->f2();
return 1;
}
都能正常运行,但如果f1()涉及到Entity的成员变量,就会发生运行错误。
补充:可以在f1()中加入一个判断条件避免运行错误:
if (this == nullptr)
return;
关于向前引用声明
教材中提到:“当一个类(类B)的对象作为另一个类(类A)的成员时,类B必须在类A后定义,这时就需要进行向前引用声明”。随后给出下列代码
class B;
class A
{
public:
void f(B b);
};
class B
{
void g(A a);
};
VS中编译通过。但是:如果在类A中尝试定义函数f(),就会发生编译错误“使用了未定义类型B”,在类A中声明成员变量B b;也会报同样错误;
而在类A中声明指针类型或引用类型的成员变量 B* b1; B& b2; ,编译通过。
刚接触到类犯的错误:
设有一个Entity类,语句“Entity e();”
不是用Entity类的默认构造函数实例化一个名为e的Entity类对象
而是“声明了一个函数名为e,返回类型为Entity类,不接收参数的函数”。
析构函数
主要用于释放类中申请的堆区内存
关于构造函数的调用
(来自https://www.bilibili.com/video/BV1et411b73Z?p=107)
视频中提到显式法调用有参数的构造函数
Entity e1 = Entity(10);
Entity e2 = Entity(e1);
刚开始以为“=”右侧先创建了一个匿名对象,再复制给左侧用默认构造函数创建的e1、e2对象。但经过验证,以上两行代码的效果等价于下方代码,并没有调用复制构造函数。
Entity e1(10);
Entity e2(e1);
实际生成了匿名对象的代码:
Entity e;
e = Entity(50);
对象e由默认构造函数生成,第二行代码创建了一个匿名对象,并赋值给e,随后匿名对象析构。等价于下方代码。
Entity temp(50);
e = temp;
//释放temp
Entity(10);
单独列出会创建一个匿名函数,在该行代码结束后立即销毁。仅适用于有参构造函数。
Entity e1;
Entity(e1);
编译器会报错“重定义”
隐式转换法
Entity e1 = 1000;
Entity类中要有接受对应类型参数的构造函数,只适用于只接收一个参数的构造函数。
复制构造函数
形参必须是(const 类名 & 对象名),没有const修饰符会被视为“接收一个Entity类型参数的构造函数”。
属于同一个类的对象之间可以互相视作friend,所以复制构造参数可以访问传入的对象的私有成员。
复制构造函数的调用时机
1、使用一个类对象初始化另一个类对象时
Entity e1;
Entity e2 = e1;
注意:Entity e2; e2 = e1; 是赋值(operator=),不会调用复制构造函数。
2、以传值的方式传入函数时
void f(Entity e){}
函数中的形参e未初始化,所以会调用复制构造函数,用传入函数的实参初始化形参e。
3、作为返回值时
Entity f1(Entity & e)
{
return e;
}
Entity f2(Entity & e)
{
Entity temp;
temp = e;
return temp;
}
调用f1()、f2()都只会调用一次复制构造函数,是在函数执行完毕即将return的时候,函数中实例化的类成员都得销毁,如f2()中的“temp”,所以得用一个匿名对象保存函数的返回值。复制构造函数用于初始化该匿名对象。
f1()会调用一次析构函数(针对匿名对象),f2()会调用二次析构函数(针对对象temp和匿名对象)。先调用复制构造函数,再调用析构函数。
this指针
功能:
1、消除类成员函数的形参与类成员变量重名的矛盾。类的成员函数中,对于类的成员函数都隐含使用了this指针。下方代码
class Entity
{
int a;
public:
void f(int a)
{
a = a;
}
}
等价于
class Entity
{
int a;
public:
void f(int a)
{
this->a = a;
}
}
如果成员函数中的形参与类成员变量重名,且形参在该函数中作为左值(例如形参以引用方式传入时可能会作为左值参与运算),编译器虽然不会报错,但为了达成目的,就得对类成员变量显式使用this指针。
//传入形参a接收函数f()的计算结果,函数f()返回bool类型用于做判断
bool f(int& a)
{
a = this->a;
return true;
}
2、 用于链式编程
Entity& f1()
{
return *this;
}
Entity* f2()
{
return this;
}
可以以这种方式调用f()函数
Entity e;
e.f1().f2()->f1().f1().f2()->f2();
成员函数f()运行结束后返回的还是调用该函数的对象自身,上方的代码中因为f1()、f2()没有进行任何操作,所以e.f1()等价于e。
3、书中的一个示例:避免对自身赋值
void A::copy(A& aa)
{
if (this == &aa)
return;
*this = aa;
}
静态成员变量
同一个类的所有实例化对象都共享相同的静态成员变量。
静态成员变量必须初始化,初始化时前面不加static,避免与一般静态变量或对象混淆。
在Entity类内:
static int a;
在类外初始化:
int Entity::a = 10;
补充:被static修饰的变量范围仅限于该cpp文件,其他文件不可见,所以也无法用extern声明另一个文件中的静态变量
未初始化的静态成员变量会导致编译错误(无法解析的外部符号),但编译器无法定位到错误位置。
静态成员函数
疑问:既然普通成员函数也能访问静态成员变量,为什么还需要静态成员函数?
(来源:https://www.zhihu.com/question/34018632)
答:访问静态成员变量的同时避免对类对象实例化,以此节省时间和空间。
所以私有的静态成员变量一般通过静态成员函数访问。
可以在类内定义或类外定义。在类外定义时不能使用static关键字。
静态成员函数不属于任何一个对象,所以也没有隐含this指针。
静态成员函数可以直接访问静态成员变量,若想访问非静态成员变量,必须通过参数传递的方式获得相应对象。
void Entity::show(Entity& e)//sta为Entity类的静态成员变量
{
cout << e.a << endl;
cout << sta << endl;
}
在main()中调用
Entity e;
Entity::show(e);
补充:私有静态成员函数无法直接访问,为什么要将静态成员函数声明为私有类型?
(来源https://bbs.youkuaiyun.com/topics/395659923?page=1)
总结:实际项目中应该尽量把函数细化,使每个函数成为一个简洁、明确的“动作”。所以对于一个功能较杂的静态函数,可以按功能将其拆分为多个子函数,将不想让用户调用的部分设为私有类型。
常成员
常对象
const Entity ce;或 Entity const ce;
常成员函数
const修饰函数时只能用于类的成员函数。
函数声明和定义中都要有const。
const成员函数可参与重载,即void f();与void f() const;可同时定义。编译器根据调用的对象是否为常成员来区分。即:若条件允许,常对象调用常版本函数;一般对象调用普通版本函数。如果只有常成员函数,则一般对象也能调用该常成员函数。
void f()const //mutable int sta;
{
cout << sta++ << endl;
}
被mutable修饰的变量可以在const函数中作修改。
友元
可以访问类的私有成员的函数或其他类
有三种实现方法
1、 全局函数
2、 类
3、 成员函数
全局函数:
将函数的声明复制到类中,并在声明前加上“friend”关键字修饰。(友元不受其所在类的声明区域public private 和protected 的影响)
除非只访问类的私有静态成员,否则作为友元的全局函数通常接收一个相应的类类型参数。
类:
在A类中声明friend class B; B类即可访问A类的私有成员。
被声明为友元的类通常使用自己的成员函数访问其他类的私有成员,所以使用规则方法与全局函数一样。
成员函数:
friend void B::func(const A &a);
相比于全局函数多了一个类命名空间。使用方法同上。
友元类不可传递、不可继承、是单向的。设前提:A类声明B类为友元
不可传递: B类声明C类为友元,C类不是A类的友元;
不可继承: B类的子类不是A类的友元;
单向:A类不是B类的友元,不可访问B类私有成员。
对象数组
Entity eArray[5] = { Entity(1) ,Entity(2) ,Entity(3) };
eArray[]中有5Entity对象,前3个使用带参数的构造函数,后2个使用默认构造函数。
成员对象(一个类(设为B)作为另一个类(设为A)的成员)
类(A类)中必须有对成员对象的初始化,如果成员对象所属类(B类)没有默认构造函数,则必须在B类的构造函数中显式初始化成员对象;如果成员对象有默认构造函数,在A类实例化时会自动初始化成员变量。
成员变量按声明顺序初始化。成员变量初始化完成后,才执行A类构造函数。