C++基础知识(总结)
1…c++ 是面向对象的编程语言(考虑扩展维复用)
2.三大特性 : 封装 继承 多态。
3.C++ 类和结构体的区别,主要体现在默认访问属性上。
类默认访问属性为private,结构体默认访问属性为public。
3. 类 – 定义: 具有相同行为(函数 )和属性(成员变量 )的个体(对象)的抽象。类系统生成的默认无参构造是公有的.
3.1 类的成员的访问属性。public protected private 默认 --> private 外部不可见 只能在本类内可见
3.2 构造函数 函数名与类名一样 无返回值。种类分为无参构造,有参构造和拷贝构造。
系统默认生成的规则。当没有任何构造的时候(包括没有拷贝构造 ) , 系统会默认生成无参构造。
有任何的构造,包括拷贝构造,就没有系统默认生成的无参构造。
系统默认的拷贝构造。 只有自己写了拷贝构造系统才不会自动生成拷贝构造。否则系统会生成拷贝构造,并且是一个浅拷贝。
3.3 浅拷贝和深拷贝 , 主要说的是指针的成员.
浅拷贝就是只是对指针成员简单的赋值, 危害导致共用空间, 当回收空间时会多次释放而造成崩溃。
深拷贝指针成员并不是简单的赋值,而会分配独立空间。自己用自己的 , 不会出现多次释放的问题。
3.4 初始化列表 : 用来初始化成员。初始化引用成员 , 初始化const属性成员 , 还可以初始化父类, 以及初始化对象成员。
class A
A(){ cout ;}
{public:
};
class C
C(){ cout ;}
{
};
class B: public A
{
int b ;
C c; // 对象成员
public: B() /* 隐含初始化 A() , c() */{ cout; } //隐含初始化 父类和对象成员
}; 定义 B b; 如果构造都有输出 , 那么输出就是 A C B
初始化列表是先于构造函数的。
class A
A(int val ): b(val) , a(b){} //初始化的顺序 和初始化列表里面写的先后顺序无关 , 与定义成员的顺序有关
int a; //先初始化a 然后是b
int b;
{public:
};
A a( 3); cout << a.a <<" " << a.b <<endl; //输出 不确定 , 3
虚表的虚指针是谁来赋值的? 是构造函数, 而不是初始化列表 --> 构造函数调用虚函数是一个静态联编, 只能调用自己的函数, 无法多态 – 构造函数内部不能实现本类与子类之间的多态
父类的构造早于子类的虚指针的赋值。
class A
virtual void init () { cout<<"A::init()";};
{public:
A() { init(); cout<< "A" ;} // init() 构造函数内调用 , 是静态联编 这个位置 子类的虚指针没有赋值, 所以调用父类
virtual void init () { cout<<"A::init()";};
};
class B :public A
virtual void init () { cout<<"B::init()";};
B() /* : A() */ { /* 虚指针赋值*/cout<< "B"};
{public:
};
B b; 输出 A::init() A B
拷贝构造: 当用一个类的对象初始化另一个类的对象时,调用拷贝构造。
A a;
A a1 = a; // 拷贝构造
A a;
A a1(a); // 拷贝构造
A a;
A a1;
a1 = a; // 赋值运算符函数
调用拷贝构造的常用的三个场景。
1.用一个类的对象初始化另一个类的对象。 2.形参是对象 , 给形参初始化 3. 返回值是对象, 给返回值初始化
都会调用什么函数?
class B;
B play( B b = bb )//2.调用拷贝构造
{
return b; //3.返回值的位置会给返回值拷贝构造
};//4.局部b对象要析构
执行语句
B bb ; // 1.调用B的构造
play( bb ); //5.该行结束, 会回收临时对象 , 这个位置会有一个析构
{
}//6.bb局部对象回收
explicit 关键字 使用explicit关键字修饰的构造函数, 可以避免不应该的隐式转换的发生。
3.5 析构函数 没有参数 没有返回值 ~类名 一般用来回收成员的 . 如果是父类, 最好写虚析构.
父类指针 = 子类对象;
回收时 delete 父类指针, 如果不写虚析构, 那么只会回收父类的空间 , 就会内存泄露
3.6 const属性 修饰成员属性, 修饰成员函数, 修饰对象 const在定义的时候要给初值.
const int p; --> p 不可变
int * const p; --> p不可变
const int const p; --> p和p都不可变 总结 就是 const 右边是谁 谁就不可变
const C中 是只读变量 C++ 就是常量 作用时期: 编译期有效, 用来做检查, 不可变. 是可以强制修改的.
C++下面
const int m = 4;
int * pm = (int*) &m;
*pm = 3;
cout << m << "," << *pm <<endl; //输出: 4, 3 m的值是常量表里面拿出来的 .
const的成员属性要在初始化列表里面给初值.
const修饰成员方法. this指针 是指向对象的地址
int A::GetA( /* const A* cosnt this /) const 常函数
return m_A; // 是不可以修改属性 因为隐藏参数 const A A*不可变 所以无法修改属性. {}
const修饰对象 , 常对象 1.不可以改变属性 2. 只能调用常函数
const A a;
a.m_A = 3; 修饰对象 , 对象是不可以改变属性 , 常对象只能调用常函数.
普通对象是可以调用任何类内函数, 包括常函数 .
左值 右值
左值 是表示 能够在 = 左边和右边的表达式
右值 是表示 只能够在 = 右边的表达式
3.7 static属性 修饰成员属性 修饰成员方法 修饰对象 修饰类外的普通函数
修饰成员属性 : 放在全局区, 类共用一份, 不能被继承(共用关系 ) , 不依赖对象存在的. 要在类外初始化,
不能在.h中初始化 ,因为会重定义 需要在.cpp文件中初始化
修饰成员方法: 类共用一份, 不能被virtual 修饰, 因为共用, 不能直接调用非静态的成员属性 , 想要调用就要依赖对象, 只能直接调用静态成员.
因为他不依赖对象, 非静态成员他们再调用时依赖this指针, 也就是依赖对象.
对象是可以调用静态成员方法的.
class A
{
public: staic const int val = 0;
};
static 对象时和变量理解差不多, static 修饰全局变量 , 只能在当前的文件中, 作为全局变量使用, 生命周期是一直到程序结束
修饰的变量不支持extern static 修饰全局函数 , 只能在当前文件中使用。
3.8 inline属性
适用于代码少. 调用频繁的场合.在调用处展开, 是空间来换时间, 编译期有效。
virtual 虚函数可以和inline一起使用吗? 答案是不可以的. 因为虚函数时动态联编, 运行时有效. 不可以修饰同一个函数的.
3.9 virtual属性
用来修饰虚函数, 实现虚函数多态.
3.10 友元 friend 可以定义友元类和友元函数, 把自己对外不可见的函数或成员属性, 给你用
class B;
class A
{
public :
void Myprint() { B b; b.fun(); } //友元类可以访问该私有成员
};
class B
{
private: fun() {}
public:
friend class A;//友元类
friend void test() ;//友元函数
};
void test()
{
B b; b.fun(); //友元函数访问私有成员
}
总结: 友元, 可以使用类的私有和受保护成员, 但是友元函数一定不是类的成员函数, 想要使用成员, 必须依赖对象.
友元不能被继承 , 友元是破坏封装的, --> 私有属性 可以使用公有get方法。
3.11 纯虚函数 虚函数= 0 ; 含有纯虚函数的类叫抽象类, 全是纯虚函数的类叫接口类
性质: 含有纯虚函数的类, 不可以实例化对象的, 要求子类继承后, 完成了所有纯虚函数的实现, 子类才能定义对象.
3.12 操作符函数
可以重载操作符 CMyString 仿写Cstring operator = 赋值重载操作符 只能类内重载.
总结
空类默认生成的成员函数: 默认的无参构造, 析构, 拷贝构造, =运算符 , &运算符
不可以被virtual修饰(不能被继承的函数): 类外的普通函数 , inline , static , friend , 构造。
二,类之间的关系
横向的有组合 依赖 聚合 关联。纵向有继承。
- 继承 : 父类所有属性和方法复制一份给子类用。
- 重载 重写(覆盖 ) 重写 override 可以了解一下
重写 要求函数名参数,返回值一模一样。父类函数要有virtual修饰.
隐藏
区别 重载 , 相同作用域下 , 函数名一样参数列表(个数或者种类 )不一致。就是重载返回值没有要求。 - 多态 – 代码所具有多种形态。代码不变,功能可变。
静态多态和动态多态。
静态多态可以通过重载 , 或模板函数 ( 不是模板类 )来实现编译时有效。 ( 模板类是一个迟后编译 )
动态多态一般也叫动态捆绑, 或动态联编。虚函数多态来实现。在运行时有效。
3.1 虚函数多态的实现过程。
1)父类中有虚函数。父类就有一张虚函数的虚表(虚函数的地址数组)。子类继承父类。子类也有一份儿虚函数的虚表。
2)子类重写父类虚函数。子类的虚函数就会覆盖虚表中父类的虚函数地址.
3)定义子类对象。在子类的前四个字节就会有一个虚指针。指向子类的虚函数表。父类指针指向子类对象。
4)通过父类指针调用虚函数。那么就会根据子类的前四个字节, 找到虚表的入口地址。然后遍历虚表。找到对应的虚函数。
完成函数调用。进而实现虚函数多态。
为什么是运行时有效? 因为是取对象的前四个字节, 拿到虚指针. 对象的定义是在运行时才能确认地址.
3.5虚函数例题
class A
{
public:
void AA(){}
virtual void BB() {}
分区 0106 的第 15 页
virtual void BB() {}
};
class B : public A
{
public : void AA() {}
void BB(){}
virtual void CC(){}
};
A*pa = new B;
pa->AA(); //调用 父类的AA
pa->BB(); // 调用 子类的BB
口诀: 调用函数的时候,首先看指针类型里边的函数是不是虚函数? 是虚函数, 找到对象前四个字节。看虚表中有没有重写。
不是虚函数, 调用该指针类型里面的函数。
虚函数多态的优缺点: 优点 : 代码不变 , 功能可变 . 缺点 , 效率低 , 破坏封装 , 父类无法调用子类特有的虚函数的 . (继承) 额外的空间开销
class A
{public:
void AA(){}
virtual void BB() {}
};
class B : public A
{
public : void AA() {}
void BB(){}
virtual void CC(){}
};
class KKK
{
public void BB(){}
};
class QQQ
{
public: virtual void BB(){}
}
A*pa = new B;
( (KKK*) pa ) -> BB(); //执行 : KKK :: BB();
( (QQQ*) pa ) -> BB(); //执行 : B :: BB(); //多态 调用虚表里面的
class A
{
public: virtual void AA(){}
};
class B : public A
{
public: virtual void AA override (){} // override 用于重写的检查
};
自己实现虚表的函数的调用?
class A
{public:
virtual void AA(){}
virtual void BB() {}
};
class B : public A
{
public: virtual void AA override (){}
};
A *pa = new B; 假如 虚函数的类型 都是 virtual void AA();
typedef void (*PFUN)(); //PFUN 函数指针类型
// 思路 : 根据对象的前四个字节 拿到虚指针, 找到虚表的入口地址. 然后调用虚函数. 把虚函数地址当成一个四字节数处理
取前四个字节 *(int*)pa ; 虚表的首元素地址 数组 转化为数组 因为地址就是四个字节 我们借用int来存储
int * arr = (int *) *(int*)pa; // arr 是一个数组 --> 虚表数组 虚表的首地址 arr
PFUN AA = (PFUN)arr[0]; //虚表的第一个元素 函数地址
AA(); //B:AA
PFUN BB = (PFUN)arr[1]; //虚表的第二个元素 函数地址
BB(); // A::BB
特殊多态: 使用引来来多态
B b;
A & a = b;
a->AA() ; //可以触发多态