C++巩固笔记
@ 与类的名称完全相同
@ 特殊的成员函数
@ 默认无参,支持传参
@ 没有返回值类型,也没有返回值,不能返回void
@ 创建对象实例时执行
@ 构造函数不能显示调用
@ 构造函数可以在类内或类外定义
@ 构造函数支持重载:一个类可以有多个构造函数,通过形参个数和类型区分
@ 构造函数内不能做任何有可能失败的操作,比如执行MML命令等
@ 严禁在构造函数中创建线程,仅作成员变量的初始化工作,其他操作通过成员函数完成
@ 与类的名称完全相同,前面加波浪号~
@ 特殊的成员函数
@ 与构造函数不同,不能带有任何参数
@ 不会返回任何值
@ 删除对象实例时执行,在跳出程序前释放资源
@ 析构函数只能有一个,不能被重载
@ 不同于构造函数,析构函数可以被显示调用:Instance.~xigouFun();
@ 类 = 属性 + 方法
@ 类:基类-派生类 or 父类-子类
@ public/private/protected是属性/方法限制的关键字,出现顺序任意
@ 封装:尽量隐藏类的内部实现,只向用户提供有用的成员函数
@ 类定义结束加分号;与结构体定义一致
@ 实现:类内 or 类外
类外定义成员函数:
returnType ClassName::MemberFunc(arg1, arg2, ...) {
}
@ 类内定义时编译器会默认争取将方法定义为inline内联函数
@ 实例化:创建类的对象
@ 访问:
访问属性:对象名.public数据成员
访问方法:对象名.public函数名(参数列表)
@ 类对象的作用域、可见域以及生存周期与普通变量相同
@ new 或 malloc 申请的动态内存需要手动清理,避免内存泄漏
@ 尽量避免定义public成员:考虑类的功能,尽量减少对外接口的暴露
@ 在类中声明并定义的成员函数,自动转换为内联函数
访问\权限 | public | protected | private |
---|---|---|---|
基类 | Yes | Yes | Yes |
派生类 | Yes | Yes | No |
外部类 | Yes | No | No |
友元类/函数 | Yes | Yes | Yes |
对象 | Yes | No | No |
[1] public/protected/private:成员访问限定符
不写成员访问限定符,默认为private。类体中访问父可以出现多次,建议一次
[2] 类内,public/protected/private可以互相访问,没有访问权限的限制
[3] private成员:只能在基类内或通过友元函数访问,不能通过对象访问
[4] protected成员:在基类内/派生类内/友元函数访问,不能通过对象访问
[5] 基类对象不能直接访问基类的私有成员,否则破坏了信息隐藏的目的
[6] 基类对象可以通过基类public/protected成员友元函数访问
继承方式 | public | protected | private |
---|---|---|---|
公有继承 | public | protected | 不可见 |
私有继承 | private | private | 不可见 |
保护继承 | protected | protected | 不可见 |
@ 公有继承:基类的公有和保护成员可见,私有不可见。基类的私有成员可以通过基类的公有和派生成员来访问
@ 私有继承:基类的公有和保护成员作为派生类的私有成员,派生类的派生类不可访问
@ 保护继承:基类的公有和保护成员作为派生类的保护成员,派生类的成员和有源函数可访问
@ 继承是权限收缩的过程
@ public继承不改变基类的访问权限
@ 基类private不受继承方式影响,派生类永远无权访问
@ private继承后,想恢复public/protected的访问权限,可通过两种方式
1) 使用using语句
2) 使用访问声明,base Class::member
int main()
{
Derived d;
d.show(); // 运行的是派生类的show()函数
d.Base::show(); // 运行的是基类的show()函数
Base b; // 基类直接生成实例
b.show(); // 运行的是基类的show()函数
return 0;
}
int main()
{
B b;
b.func1();
A::func2(); // 静态方法
A *a;
a = new B;
a->func1();
a->func2(); // 静态方法
return 0;
}
class C : public A, public B {
...
};
A->D, B->D, C->(A,B),多继承方式:
class D {
...};
class A : public D {
...};
class B : public D {
...};
class C : public A, public B {
...};
继承时会使D创建两个对象,可使用虚继承
class D {
...};
class A : virtual public D {
...};
class B : virtual public D {
...};
class C : public A, public B {
...};
@ 只用于包含静态成员的类型,不能进行实例化
@ 为什么要有静态类:防止继承,防止外部进行new操作
@ 静态类用于无需创建类实例就能访问的数据和函数
@ 静态类成员可用于分离任何对象标识的数据和行为,对象变更不会影响这些数据和函数
当类中没有依赖对象的数据和行为,就可以使用静态类
当一个类完全脱离实例数据和对象时就可以使用静态类
@ 静态类可以有自己的成员变量和函数,但都必须是静态的
@ 静态类没有基类
@ 类内static关键字修饰,初始化时不能再加static
@ 静态成员变量和普通静态变量一样,存储在全局区,不占用对象内存,用于数据共享
@ 静态成员变量属于类,但不属于具体的对象,所有对象均可访问
@ 静态成员变量只能在类外初始化,不能在类内初始化。没有类外初始化的static变量不能使用
@ 类内static关键字修饰,类外定义时不能加static
@ 静态成员函数不能访问对象的成员,只能访问静态成员变量和静态成员函数
原因:编译器不会为静态成员函数分配this指针
@ 与静态成员变量一样,不管有没有创建对象,都可以调用静态成员函数
原因:没有this指针,无法在函数体内部访问某个对象,包括变量和函数
@ 静态成员函数和普通成员函数的根本区别
普通成员函数:有this指针,可以访问类中任意成员
静态成员函数:没有this指针,只能访问静态成员
选择最合适的重载函数或重载运算符的过程,称为重载决策
returnType operator 运算符 (参数)
#include <iostream>
using namespave std;
class Demo {
public :
// 友元函数形式实现运算符重载
friend Demo & operator+ (Demo &demo, int n);
Demo (int a)
{
this->a = a;
}
int GetA()
{
return a;
}
private:
int a;
};
// 重载"+"运算符
Demo & operator+ (Demo &demo, int n)
{
demo.a += n;
return demo;
}
int main()
{
Demo demo(1);
// 重载"+"运算符后,对象可以直接加一个数
demo = demo + 2;
cout<<demo.GetA()<<endl;
return 0;
}
class A {
A *p;
p->func();
(*p).func();
A p;
p.func();
};
void B::DoSomething()
{
A a;
a.PrintMe();
}
@ 虚函数存在的唯一目的是为了实现多态
@ 声明:virtual returnType Function();
@ “推迟联编/动态联编”:编写代码的时候不能确定被调用哪个的是基类的函数还是那个派生类的函数,所以称为虚函数
@ 基类虚函数必须实现,否则链接时会报错
@ 虚函数,基类和派生类各有版本,多态方式动态绑定
@ 虚函数重写:基类中可以是private,派生类中可以是public或protected
@ 定义纯虚函数的目的在于:使派生类仅仅继承函数的接口;
@ 纯虚函数引入是为了解决“要使用动态特性,但基类本身生成对象不合理”的情况,如动物类实例
@ 声明:virtual returnType Function() = 0;
纯虚函数的声明在于告诉派生类设计者:你必须提供一个纯虚函数的实现,但我不知道你会怎么实现它
@ 声明了纯虚函数的类是一个抽象类
@ 抽象类不能定义对象/实例,但可以声明只想该抽象类的具体类的指针或引用
@ 抽象类作为基类,纯虚函数的实现由派生类给出,如果派生类未重新定义纯虚函数只是继承,则派生类仍然是一个抽象类,而不是一个具体的类
@ 派生类实现纯虚函数后,该纯虚函数在派生类中就变成了虚函数,派生类的派生类可重新该函数
error: cannot declare varible 'inst' to be of abstract type 'Class'
note: because the following virtual funcs are pure with 'Class'
@ 默认参数可以放在定义或声明中,但二选一。最好放在声明处便于引用
声明和定义都有,会有编译错误
大部分情况下,别人调用哪个代码只能看到函数声明
写在定义处,使用该函数前要把定义放在前面,否则编译无法确定该函数是否默认参数
@ 如果某个参数是默认参数,那么它后面的参数都必须是默认参数
即:如果函数含有默认参数,那么最后一个参数一定是默认参数
@ 不要重载一个带默认参数的函数 或 函数重载时谨慎使用默认参数
含有默认参数的函数调用存在歧义,举例如下
默认参数可将一系列简单的重载函数合成一个
@ 默认参数不仅可以用常数,还可以用任何有定义的表达式作为参数默认值
/* 例1:带默认参数函数重载,调用存在歧义 */
class Test {
public:
int func(int a)
{
return a;
}
int func(int a, int b = 1)
{
return a + b;
}
};
func(2); // 不知道是调用第一个还是第二个带默认参数的函数
/* 例2: 表达式作为参数默认值 */
int Max(int m, int n);
int a = 1, b = 2;
void Function(int x, int y = Max(a, b), int z = a * b)
{
...
}
Function(4); // 正确。等效于Function(4, Max(a, b), a * b);
Function(4, 9); // 正确。等效于Function(4, 9, a * b);
Function(4, 2, 3); // 正确
Function(4, , 3); // 错误。省略的默认参数一定是最右边连续的几个
va_list args; // 创建一个可变参数列表
va_start(args, num); // 初始化args指向强制参数的下一个参数
va_arg(args, type); // 获取当前参数内容并将args指向下一个参数,for循环获取
va_end(args); // 释放args
@ 使用到三个宏:va_start, va_arg, va_end,va_list只是一个char指针
typedef char *valist ...
@ 由于开始的时候从右至左把参数压栈,va_start传入最左侧的参数,往右的参数依次更早被压入栈,一次地址依次递增(栈顶地址最小)。va_arg传入当前需要获得的参数的类型,便可以利用sizeof()计算偏移量,一次获取后面的参数值
@ 函数调用过程中参数传递是通过栈实现的,从右到左的顺序将参数压栈
参数之间是相邻的,知道前一个参数的类型及地址,根据后一个参数的类型就可以获取后一个参数内容,以此类推可实现可变参数函数所有参数的访问。
@ 如果入参少了(<num)则会访问到参数以外的区域,出现异常
/* 参数访问过程 */
#include <stdio.h>
void debug(unsigned int num, ...)
{
unsigned int i = 0;
unsigned int *addr = #
for (i = 0; i <= num; i++) {
/* 从右往左依次取出传递进来的参数,相当于从栈底到栈顶 */
printf("i=%d, value=%d\r\n", i, *(addr + i)); // 压栈过程地址递减
}
}
int main(viod)
{
debug(3, 66, 88, 666);
return 0;
}
2020.07.18 create by shuaixio