1、指针与数组,引用和常量
1、指针函数和函数指针
1.指针函数:
#include<stdio.h>
#include<stdlib.h>
int* func1() {
int c = 10;
int* pa = &c;
return pa;
}
int* func2(){
int b = 22;
int* pb = &b;
return pb;
}
int main() {
int* pa = func1();
printf("%p\n", pa);
printf("%d\n", *pa);
int* pb = func2();
printf("%p\n", pb);
printf("%d\n", *pb);
return 0;
}
说明:对于上述代码,函数在调用结束后自动销毁,因此pa
和pb
只保留了两个函数中a
和b
的两个的地址,地址里面的值是多少不确定,因此这种写法是不对的。
正确的方法:
在堆区开辟堆区变量,将值保存到堆区里。
#include<stdio.h>
#include<stdlib.h>
int* func1() {
int c = 10;
int* pa = (int*)malloc(sizeof(int));//堆区开辟空间
*pa = c;//将c的值保存到堆区开辟的地址
return pa;
}
int* func2(){
int b = 22;
int* pb = (int*)malloc(sizeof(int));
*pb = b;
return pb;
}
int main() {
int* pa = func1();
printf("%p\n", pa);
printf("%d\n", *pa);
int* pb = func2();
printf("%p\n", pb);
printf("%d\n", *pb);
free(pa);//手动释放pa堆区开辟的空间
free(pb);
return 0;
}
说明:堆区变量只能有程序员手动申请,释放(手动释放)(所有程序结束后释放)。
2、函数指针
int main() {
int a[] = { 2,3,1,4,4 };
printf("%p\n", a);
printf("%p\n", &a);
return 0;
//输出:0000006C6797F988
// 0000006C6797F988
由上述代码可知,数组a
和&a
的地址是一样的,函数也类似。注意(int * p = &a是错误的)
double (*pa)(double);
pa = func;
printf("%p\n", func);
printf("%p\n", &func);
printf("%p\n", pa);
//输出:00007FF6148B13E3
// 00007FF6148B13E3
// 00007FF6148B13E3
//三者的输出相同
double func(double a) {
a = 10.232;
return a;
}
int main() {
double c = 0;
double (*pa)(double);//*pa 就等价于func //(pa是一个指针指向符合参数列表为double,返回值为double的函数)
pa = func;//将函数指针指向func等价于pa = &func
double a = func(c);
printf("%lf\n", a);
printf("%lf\n", pa(c));//pa(c)等价于func(c)
return 0;
}
//输出:10.232000
// 10.232000
说明:对于int(*pa)(int) = NULL
如果没有括号,即int *pa(int)
就变成一个指针函数,返回值是 int*
,
**(*pa)
告诉大家,我是一个指针,
(*pa)(int)
后面括号是表示我是一个指向函数的指针,参数是int类型
int(*pa)(int)
表示返回值是int.
2、指向常量的指针和常指针
1、指向常量的指针
int const * pt = &a
等价于const int* pt = &a
以*右侧为界,右侧pt
的类型是一个指针,指向const int
(常量)因此,该值不能通过*pt
进行修改。
int a = 3;
const int* pt = &a;
*pt = 6;//错误,$$左值为不可修改左值$$
int *p3 = &a;//true
//"const int *"类型的值不能分配(初始化)"int *"类型的实体
int *p4 = pt;//错误,pt指向是常量,如果相等,就等于p4间接通过pt改变a的值
int b = *pt;//true
const int cv1 = 200;
const int cv2 = 4;
const int *pc1 = &cv1;//true
const int *pc2 = &cv2;//true
int *p1 = pc1;//错误
int v1 = cv1;//true
int v2 = *pc1;//true
2、常指针(必须初始化)
普通指针:
T *p[= exp];
指向常量的普通指针:
const T *pt[= exp];
指向变量的常指针:
T * const pt = exp;
(必须初始化)//因为常指针必须指向一个已经开辟的空间,如果不初始化,相当于没有开辟指针指向的空间。
指向常量的常指针:
const T * const pt = exp;
(必须初始化)
3、关于字符串的说明
char 和char[]定义字符串*
char* str = "This is a string!";
等价于const char* str = "This is a string!";
//指向常量的指针,字符串里面的内容不可以修改;
char str[] = "This is a string!";
//str
相当一个变量指针,字符串里面的内容可以修改;
3、关于指针的一些说明
对于**T *p = ...
**: p+n等价于(T*) (( (char*)p + sizeof(T)*n ))
将p转化为字节,再加上n类型T所占的字节长。
4、指针数组和数组指针
对于数组的理解:int * array[5]理解成int*[5] array
意思是array变量是一个数组,数组里是指针
指针数组:int* array[5];
数组指针:int (*p)[5];
理解成 int[5] *p
p是指针指向一个数组;
//例
int a[4] = {1,2,3,4};
int *b[4] = {&a[0],&a[1],&a[2],&a[3]};
int (*p)[4] = &a;
cout<<(*p)[3]<<endl;
5、引用
引用的含义:
是一个别名;对应的变量\对象,必须存在;必须初始化
int a =10;
int &b = a;//a必须初始化
引用的价值:
**指针和引用的差异:**存取值的方法,初始化,对象或变量的存在性
//使用变量a和b
int a =7,b = 8;
void f(int a,int b){cout<<a<<b<<endl;}
//C语言下,修改变量a和b
int a = 7,int b = 8;
void f(int* pa,int* pb){*pa =1;*pb =2;}//调用方f(&a,&b)
//使用引用,修改变量a和b
int a = 7, b = 8;
void f(int &a,int &b){a = 1;b = 2;}//调用方f(a,b)
6、#define和宏
#define的缺点: 无类型检查
优点:可用于条件编译
可使用#,##,#@
可使用_LINE_,_FILE_,_FUNCTION_
//条件编译
#define DEBUG
int main(){//预编译时执行,相当于main 函数中只有第4行和第6行其他的没有
int a = 100;
#indef DEBUG
cout<<a<<endl;
#else
Do2();
#endif
}
//#,##,#@
#define STR(x)#x //将x转换为字符串"x"
#define VAR(x)n##x //VAR(x)代替名称为nx的。。。
#define TOCHAR(x)#@x//将x转换成字符'x'
int main() {
int n1 = 1, n2 = 4, n3 = 7;
const int *n4 = &n2;
cout << STR(123) << endl;//输出“123”
cout << STR(abc) << endl;//输出"abc"
cout << VAR(2) << endl; //输出4
cout << TOCHAR(2) << endl;//输出'2'
cout << TOCHAR(a) << endl;//输出‘a’
return 0;
}
7、const
和引用、命名常量
命名常量:
格式:const int CARD_COUNT = 54;
//xx.h
const int v1 =100;
const int v2 = 200;
//a.cpp
#include"xx.h"//a.cpp中包含了一次v1,v2的定义
...
//b.cpp
#include"xx.h"//b.cpp中包含了一次v1,v2的定义
...
上述a.cpp
和b.cpp
链接时不报错,因为常量堆叠,相当于将v1用100替换,v2用200替换
const
和引用:
变量的引用:
//变量的引用
int val = 100;
int& myval1 = val;
const int& myval2 = val;//不能通过myval2改变val的值
myval2 = 300;//错误
myval1 = 200;//通过myval1改变,val和myval2的值都会改变
//常量的引用
int b = 100;
int& aa = b;//可以通过aa改变b的值
const int& aa = b;//不可以通过aa改变b的值
const int& bb = 1;//ok。。。
//函数的引用
int var = 100;
const int cvar = 200;
void f(int a1, int& a2, const int& a3) {
}
int main() {
//f(1, 2, 3);//错,int& a2是一个变量,2是一个常量
f(1, var, var);//ok
//f(1, cvar, cvar);//错,a2是变量,cvar是常量
f(var, var, var);//ok,a3是常量,传入var表示不能通过a3改变var的值。(函数内不允许改变a3的值)
f(cvar, var, cvar);//ok
return 0;
}
2、类型与变量
1、oop
语言中的类型
基本类型
自定义类型:ADT
(抽象数据类型)
**泛型:**以类型为参数的类型(参数化的类型)
template<typename T, typename Y>
元类型及元对象:各类型的类型
2、C++内置类型
基本类型:char ,wchar_t(宽字符),int ,float,(void)
#include <iostream>
#include <string>
int main() {
wchar_t wideChar = L'文'; // 使用前缀L表示宽字符
std::wstring wideString = L"宽字符文本";
std::wcout << wideChar << std::endl; // 输出宽字符
std::wcout << wideString << std::endl; // 输出宽字符串
return 0;
}
基本类型的扩展:<类型修饰>基本类型
类型修饰:short,long,signed,unsigned,double
long int,unsigned long int,double float(等价于double),long double...
bool
型
int a = 0,b = 2;
bool res(a>b);
if(res){
cout<<"true"<<endl;
}else{
cout<<"false"<<endl;
}
编译器推导类型(auto ,decltype
)
#include <iostream>
using namespace std;
#include <string>
template<typename A, typename B>
auto multiply(A a, B b) -> decltype(a* b) {//在这个例子中,-> decltype(a * b)指定了函数multiply()的返回类型为 return a * b; //decltype(a * b),也就是a * b表达式的类型。
}
int main() {
int a = 10;
auto b = a + 99;
cout << multiply(a, b) << endl;
return 0;
}
使用函数后置返回类型的语法形式有几个优点:
- 允许返回类型依赖于函数参数或其他上下文,特别适用于模板函数。
- 可以在函数参数列表之前使用参数名,使得在返回类型的表达式中可以引用到这些参数。
- 可以更清晰地表示复杂的返回类型,避免长长的返回类型表达式出现在函数声明的前面。
3、使用typedef
的自定义类型
定义类型格式:typedef<已知类型><新类型>
定义函数类型格式:typedef ReturnType(*新类型)(参数列表);
typedef
本质上没有增加新类型
typedef unsigned char UCHAR
typedef unsigned long DWORD
typedef unsigned int uint
UCHAR ch = 'a';//等价于unsigned char ch = 'a';
//其他同理
#include <iostream>
using namespace std;
#include <string>
int f(int, int) {}
int g(int, int) {}
typedef int (*MyFuncType)(int, int);//通过typedef关键字,将函数指针MyFuncType定义为接受两个int,
MyFuncType f1 = f; //返回 int 类型的(将函数类型化)
MyFuncType f2 = g; //MyFuncType类似一个指针数组,
int YourFunc(MyFuncType ff) {
return ff(88, 99);
}
int main() {
f1(1, 2);
f2(3, 4);
YourFunc(f);
YourFunc(g);
return 0;
}
4、枚举类型
1、(C++98)
枚举类型:enum 枚举类型名{枚举值列表};
枚举值必须是整数;
第一个枚举值,缺省为0;
后一个枚举值,若没有指定,则为前一个枚举值+1
enum WeekDay {
MON = 1,TUR,WED,THU,FRI,SAT,SUN = 0
};
int main() {
WeekDay day1, day2;
day1 = SUN,day2 = THU;
cout << day1 << endl;
int num = day2 + 100;
cout << num << endl;
return 0;
}
C++98下的说明:
1、内部就是int
2、int与枚举值课随意切换,不安全
3、同名枚举值的问题
enum Dir{LEFT,RIGHT};
enum State{RIGHT,FAILED};
RIGHT = =0还是1==???==
2、强类型枚举(C++1z
的枚举类)
enum class枚举类名{枚举值列表}
;
枚举值可以指定类型,指定数值
枚举值不再允许与int随意转换,比较
使用时必须指明scope(即枚举类名)
enum class AAA {
A = 'a',
EF = 120
};
int main() {
char ch = (char)AAA::A;
cout << ch << endl;
cout << (char)(AAA::A) << endl;//使用必须加上作用域,输出(赋值)必须强制类型转换
cout << (int)(AAA::EF) << endl;
return 0;
}
3、结合class和enum
的常量
class My{
public:
enum Dir
{
LEFT,RIGHT
};
};
int f(My::Dir dir) {
return dir;
}
int main() {
f(My::RIGHT);//枚举类型的底层是int,因此存在My::RIGHT向My::Dir的隐式转换
return 0;
}
在C++中,枚举类型的底层类型是可以指定的,默认情况下是int
类型。因此,枚举常量的值可以隐式地转换为其底层类型。
在给定的代码示例中,My::RIGHT
是一个枚举常量,其底层类型是int
。当将My::RIGHT
作为参数传递给f
函数时,发生了从枚举常量到底层类型的隐式转换。
在函数签名中,f
函数的参数类型为My::Dir
,这是一个枚举类型。然而,由于枚举类型的底层类型是int
,因此编译器可以隐式地将My::RIGHT
转换为int
类型,并将其传递给f
函数。
在函数体内部,dir
参数是My::Dir
类型,但由于底层类型是int
,可以将int
类型的值直接返回。
因此,底层存在从My::RIGHT
到My::Dir
的隐式转换,允许将枚举常量传递给接受枚举类型参数的函数,并在需要时进行相应的类型转换。
5、自定义类型
c++中,class/struct,除了默认的访问控制不同之外,完全一致
class name{
.....
};
6、导出类型
数组,指针,引用
MyClass * pobjs[4];//指针数组,数组里存放在指针
7、声明与定义
声明:解释说明一个编译单元中一个名字的含义和属性
extern int a;//声明一个外部整型变量a
extern const int a;//声明一个外部整型常量a;
int f(int);//声明函数f
struct S;//声明结构S
typedef int Int; //声明类型Int
extern X anotherX;//声明外部变量anotherX
using N::d;//声明名字d;
定义:
类型声明:
//类型声明
class Meat;
typedef unsigned int unit;
//类型定义
class Dog {
public:
private:
};
//函数声明....略
//变量声明
extern int a;
extern Dog aDog;
声明和定义的使用原则
就近原则
先声明后使用
单一定义:在一个编译单元中,同一个标识符(变量,函数,类名等)只能被定义一次;但声明可以多次
//反例1
//a.cpp
int a= 1;
int a = 1;
//反例2-a.cpp
#include"a.h"
void main{
a = 10;//在a中已经被定义一次
f(a);
}
8、变量
int a{ 10 };//等价于 a=10;
#include <iostream>
using namespace std;
//全局数据区 num(全局作用域)
int num = 999;
//全局数据区 num2(文件作用域(仅当前cpp文件))
static int num2 = 8;
void func(int a, int b, int c) {//abc压栈
//全局数据区 n(块级作用域)
static int n = 0;
//栈区(块级作用域)
int result = (a + b + c) * (++n);
//字符串"Result="全局作用域(常量)
cout << "Result=" << result << endl;
}
int main() {
int a = 10;//result栈区
static int b = 20;//全局数据区
func(a, b, 20);
//变量p本身分配在栈区,其指向堆区中的一个地址
int* p = new int(55);
}
9、表达式的特别说明
表达式都有返回值。
逗号表达式最后一个表达式结构是该逗号表达式的返回值
左值表达式返回值应该一个地址。
3、函数
1、函数的声明
即函数原型,指明函数的名字,参数个数,参数顺序,参数类型,异常等。
格式:[extern][调用约定]<返回类型><函数名>(<参数表>)[const][异常说明]
返回类型,缺省参数值不能作为区分标志
2、函数的重载和实现
条件:
函数参数的个数,类型,顺序,const
修饰,异常等不完全相同
返回值类型不作为区分标志
缺省参数不作为区分标志
值类型参数的const
型与非const
型不作为区分标志
引用和指针类型参数,是否可以改变实参,可作为区分标志
//不合法的
void Fun(); int Fun();
int Fun(int); int Fun(int&);
int Fun(int); int Fun(int= 5);//(缺省值)
int Fun(int); int Fun(const int);
int Fun(int *); int Fun(int* const);
//合法的
void Fun();
void Fun()const;
int Fun(int); int Fun(int*);
int Fun(int &); int Fun(const int &); int Fun(int&)throw();
int Fun(int*); int Fun(const int*); int Fun(int* ,int= 6);
对于void Fun()const
的解释:
函数声明中的 const
关键字表示该函数是一个常量成员函数,也就是说它不会修改对象的状态。常量成员函数可以在常量对象上调用,而不能在非常量对象上调用,以保证对象的不可变性。
例如,如果有一个类 MyClass
,并且存在一个常量对象 const MyClass obj;
,那么可以使用 obj.Fun();
调用 Fun()
函数,因为 Fun()
是一个常量成员函数,它不会修改 obj
对象的状态。
实现:
补充:
缺省参数:第一个带缺省参数的后面必须都有缺省值
void Fun(int,char ='c',int);//错误
void Fun(int,char='c',int=9);//正确
若不存在完全匹配的函数,尝试类型转换(每个参数只一次)
3、实参与形参的匹配
值传递:
int a = 10;
const int b = 20;
My mObj;
const My cmObj;
void f(int, My);
void f(const int, const My);//不能通过形参改变传入的值
f(a,mObj);//ok 与第一个匹配
f(b,mObj);//ok 与第二个匹配,mObj进行类型转化
f(a,cmObj);//ok 与第二个匹配,a进行类型转化
f(b,cmObj);//ok 与第二个匹配
指针传递:
int a =10;
My mObj;
int *pa = &a;
My*pmy = &mObj;
const int *cpa = pa;
const My *cpmy = pmy;
void f(int *,My *);
void f(const int *,const My *);
f(pa,pmy);//对应(1)
f(pa,cpmy);//对应(2)
f(cpa,pmy);//对应(2)
f(cpa,cpmy);//对应(2)
引用传递:
int a= 1;
const int &b = a;
const int c= 2;
void f(int,int); // f(a,a); f(a,b); f(a,c); f(a,3);
void f(int&,int&);// f(a,a); f(a,b);//b是常量 f(a,c);c是常量,不能被变量引用 f(a,3)//3是数,是常量,无法被引用
void f(int,const int&);// f(a,a); f(a,b); f(a,c); f(a,3)
void f(int&,const int&);// f(a,a); f(a,b); f(a,c); f(a,3);f(b,b);b是常量
void f(const int&,const int&);// f(a,a); f(a,b); f(a,c); f(a,3)
4、返回类型
按值返回:
1、默认返回int
型;
2、void
3、内置类型(基本类型,派生类型):
返回普通内置类型值(非引用,非指针)的都是const
unsigned int f()
等价于const unsigned int f();
4、自定义类型
指针:
T* f( );
等价于T* const f()
,不等价于const T* f()
因为该函数是返回的指针不能修改,即指针是常量(常指针),而const T*
是指向常量的指针,指针本身是可以修改的。
引用返回:
T&f();
不等价于const T& f();
必须返回一个有效对象的引用
int global =100;
int & other(int &,const int &);
int & f(int a,int &b,const int &c){
int Ivar = 88;//局部临时量
static int sVar = 99;//局部静态量
}
上述函数的返回值:黄色是错误的
return a;return b; return c;(c是常量) return global; ==return global+8;==是一个数,不能返回给引用 return Ivar; return sVar
; return sVar+2; return sVar +=3;return other(Ivar,b);==return other(sVar+2,global);==sVar+2数,不能被引用
//比较:取两个整数中的最大一个
int Max1(int a, int b) { return a > b ? a : b; }
int& Max2(int &a, int &b) { return a > b ? a : b; }
const int& Max3(int& a, int& b) { return a > b ? a : b; }
const int& Max4(const int& a, const int& b) { return a > b ? a : b; }
int main() {
int a = 3;
int b = 4;
int m1 = Max1(a, b);
cout << a << b << m1 << endl;//输出为344
int m2 = Max2(a, b);
cout << a << b << m2 << endl;//输出为344
Max2(a, b) = 5;//Max2的返回值为b,b变为5
m2 = Max2(a, b);//m2等于5
cout << a << b << m2 << endl;//输出为355
int m3 = Max3(a, 10);//错误,数不能被引用(没有空间)
Max3(a, b) = 6;//Max3的返回值是常量,不能被修改
int m3 = Max3(a, b);//m3 = 5;
cout << a << b << m3 << endl;//输出355
Max4(a, b) = 7;//Max4的返回值是常量,不能被修改
int m4 = Max4(a, b);//m4 = 5
cout << a << b << m4 << endl;//输出355
return 0;
}
4,文件
文本文件
二进制文件
以读的形式打开文件
#include<stdio.h>
#include<stdlib.h>
int main(){
FILE* fp = fopen("C:/Users/tone/Desktop/新建 文本文档.txt","r");
// 1.找不到文件
// 2。文件权限(读 写 执行)
// 3、程序打开文件超出上限65535;
// 以上情况会打开文件失败
if(fp==NULL){
printf("打开文件失败!\n");
return -1;
}
char ch;
//文件的字符读取
// 文件默认结尾为EOF(-1);
while((ch = fgetc(fp))!=EOF){
printf("%c",ch);
}
fclose(fp);
system("pause");
return 0;
}
5,、类
类变量和类方法
类变量:
类具有的属性或应由类中所有对象共享的属性
1)C++中用静态数据成员表示
2)类变(常)量的存储位置(程序区存放)
3)类变(常)量的初始化(必须初始化)
类方法:
类属的行为表示
C++中用静态成员函数表示
不能访问任何非静态成员(数据成员和函数成员)
没有隐含的this 指针,也不能带const修饰
常用类名作限定(最好不用对象名)
好:ClassName::OneStaticFunction();
不好: objName.OneStaticFunction();
例子:
实例变(常)量和实例方法
this 指针
1、是非静态成员函数隐含的第一个形参
2、其类型相当于T* const this
this 的作用域和生存其是在非静态成员函数的{ }内
this 指针永远指向当前对象
class Ultraman{
public:
Ultraman(){happiness = 0;}
int getHappiness()const{
return happiness;
}
Ultraman& fight(Monster& );
}
Ultraman& Ultraman::fight(Monster& ){
.....
return *this;
}
int main(){
Ultraman super;
cout<<super.fight(a).fight(b)....<<endl;
return 0;
}
外联实现
在类外实现
Car::Car(...){
....
}
内联实现:
在类内实现
在实现时使用inline关键字
内联的作用:
建议编译器在调用处直接展开函数代码,永远只是建议
inline关键字:
只在实现同时存在时,才有意义
建议放在头文件
访问控制
public:
任何类都可访问
private:
==本类(不是本对象)==或友元可以访问
封装与信息隐蔽
面向对象:针对接口建模
常成员函数:(带const修饰的成员函数)
普通成员函数的this指针类型:T* const this
常成员函数的this指针类型const T* const this
构造函数
隐式调用和显示调用
class Name{
public:
Name();
explicit Name(int);
Name(Other& other);
privite:
Name(int val,int);
...
};
void f(const Name& o){ }
int main(){
Name obj1;
Name obj2(100);//明确的显示调用
Other oth;
Name obj3(oth);
f(100);//explicit 存在,禁止隐式调用
f(obj2);//ok
Name obj4(1,2);//不对
}
f(100)
就是隐式调用,如果没有explicit
100通过隐式调用Name(int)
函数
第一组,main函数中调用Name中静态函数create,创建Name只创建一次(static),创建的在程序区
第二组,创建的在栈区
对象的初始化
1、在构造函数内通过赋值初始化
class Card {
public:
Card(int aId) {
x =0;
y=0;
}
…
private:
int x;
int y;
};
(对于const修饰的对象成员数据,用这种方法不对)
2、C++1z中,类定义时指定初值
对于实例变(常)量可以直接赋值,对于静态成员变量,只有静态整型常量可以直接初始化,其他的都不可以
析构函数
格式:
无参数,无返回值,访问控制:一般均为public,有this指针
缺省的析构函数:编译器提供一个默认(缺省)的,具有public访问控制的析构函数
对于A* pa = new A(8)
pa在栈区,指向堆区创建的对象
注意事项:
析构函数可以显式调用,但是不建议
- 显式调用析构函数的主要应用场景是在特殊情况下,**例如在自定义的内存管理或资源管理中,可能会手动管理对象的生命周期。**但这种情况应该非常谨慎,并且需要确保正确地处理对象的生命周期和资源释放。
对于局部对象显示调用析构函数是不正确的:
- 对象的析构函数可能被多次调用:当对象的作用域结束时,编译器会自动调用析构函数。如果你在同一作用域内显式调用析构函数,就会导致析构函数被调用两次,可能会引发未定义的行为。
- 可能无法正确释放资源:对象的析构函数通常负责释放对象所持有的资源。通过显式调用析构函数,你可能会绕过资源的正确释放逻辑,导致资源泄漏或其他问题。
- 一般情况下不建议对局部对象显式调用析构函数。编译器会自动处理对象的析构和资源释放,而显式调用析构函数可能会导致问题,因此应该谨慎使用,并确保正确处理对象的生命周期和资源管理。
析构和构造函数的访问
以下面代码为例:
class B{
public:
B(){
cout<<"B的无参构造函数"<<endl;
}
B(int n):num(n){
cout<<"B的有参构造函数"<<endl;
}
int get()const{
cout<<"B::get()"<<endl;
return num;
}
~B(){
cout<<"B的析构函数"<<endl;
}
private:
int num;
};
class A{
public:
A();
A(int n);
void Do(const B& aB);
~A(){
cout<<"A的析构函数"<<endl;
}
private:
int num;
B b1;
B b2;
static B globalB;
};
B A::globalB(100);//B的有参构造函数
A::A():num(0),b2(2){
cout<<"A的无参构造函数"<<endl;
}
A::A(int n):b2(n+1),num(n),b1(n){
cout<<"A的有参构造函数"<<endl;
}
void A::Do(const B& aB){
cout<<aB.get()<<endl;
cout<<"A::do()"<<endl;
}
void func(){
A a1;
A a2(5);
A* pa = new A(8);
pa->Do(2);
delete pa;
}
int main(){
A a0;
func();
system("pause");
return 0;
}
a0因为system…的原因析构函数的调用未显示,在程序执行完成后,a0释放调用析构函数
注意事项:
- 静态数据成员
globalB
类外初始化,调用B类的有参构造函数进行初始化,先于main函数 - Do(2)函数中,参数2调用有参构造函数,将int转换成B类型,如果B的有参构造函数加上
explicit
会报错,因为要求显示调用 - 函数
Do
接受一个const B&
类型的参数aB
。当以Do(2)
的形式调用时,实参1
将进行隐式类型转换,创建一个临时的B
对象。然后,该临时对象会被绑定到aB
这个引用参数上。这个临时对象在函数调用期间被命名为aB
,但在函数外部并没有其他方式来引用它。因此,可以说在函数调用期间,aB
是一个匿名对象。当函数Do
执行完毕后,这个临时对象将会被销毁。 - 对象的成员变量和静态成员变量的析构顺序如下:
- 首先,析构函数会先执行对应对象的析构函数体内的代码,即执行
cout << "A的析构函数" << endl;
。 - 接下来,按照成员变量的声明顺序,逆序依次调用各个成员变量的析构函数。
- 最后,对于静态成员变量,它们在程序结束时自动销毁,其析构顺序与它们的定义顺序相同。
- 首先,析构函数会先执行对应对象的析构函数体内的代码,即执行
拷贝构造函数和赋值函数
拷贝构造函数:从无到有创建一个新的对象,赋值函数:是赋值前,被赋值的对象已经存在
浅拷贝:
//浅拷贝说明
class AA{ };
class My{
public:
My(AA& a):mRefAA(a){ }
private:
int mVal;
AA* mpAA;
AA& mRefAA;
AA mAA;
};
深拷贝
禁止拷贝
class A {
public:
//
A (const A& a) = delete;
private:
A(const A& a);//private下,且没有实现的拷贝构造函数
const int my_a;
B& pb;
};
对象的赋值
自定义赋值函数
浅赋值的不足:
class B{
public:
//函数传参要最好以引用的方式传递,值传递会调用拷贝构造函数
B& operator=(const B& rhs){//自定义赋值函数(等号的重载)
if(&rhs!=this){ //b1赋给b1;(通过地址判断)
delete pch;
pch = new char (*rhs.pch);
}
return *this;
}
private:
char* pch;
};
int main(){
B b1;
B& b2 = b1;
b1 = b1;
b1 = b2;
return 0;
}
6.转换函数,名字空间,友元,嵌套类,流
1、转换函数
内置类型 -----内置类型
自定义类型——内置类型
自定义类型——自定义类型
class A{
};
void f1(int n){}
void f2(float v){}
void f3(const A& a){}
int main(){
int nVal = 2;
float fVal = 3.14;
f1(nVal);
f1((int)fVal);//强制转换
f2(nVal);//自动转换(int 转换为 float)
f2(fVal);
f3(fVal);//wrong(不存在int的构造函数)A是一个空类
f3(fVal);//wrong//A类中加入A(float n){}就对了
return 0;
}
说明:static_cast
:
static_cast<目标类型>(表达式或变量)
//例
int intValue = 10;
float floatValue = static_cast<float>(intValue); // 将整数类型转换为浮点类型
double doubleValue = 3.14;
int integerValue = static_cast<int>(doubleValue); // 将浮点类型转换为整数类型
void* voidPtr = static_cast<void*>(&intValue); // 将整数类型指针转换为void指针类型
需要注意的是,使用static_cast
进行类型转换时,要确保转换是合理和安全的。如果进行了不合理的转换,可能会导致未定义的行为或错误的结果。因此,在使用static_cast
时,请确保您了解源类型和目标类型之间的兼容性和转换规则。
另外,在某些情况下,可能需要使用其他类型转换运算符,如dynamic_cast
、reinterpret_cast
或const_cast
,具体取决于您的需求和转换的语义
转换构造函数
//将2,通过Fraction(int num... ,int ):....{}
转换为Fraction类型
//将a,通过Fraction(const A& a){}
转换为Fraction类型
自动转换函数://在要转换函数的类里
class T{
public:
[explicit] operator DestType()[const]{
......
return DestType;
}
};
说明:
必须有return ;
可以多个不同转换目标类型的转换函数
class和class之间相互转换,不能有二义性
两种方法的对比
一种是转换构造函数,在转换成的类中实现,即通过构造函数,将一个类转换成当前类
另一种是转换函数,在要转换的类中实现,通过operator B()
在A类中实现,即完成从A类到B类型的转换;
class B{
};
class A{
public:
//从A类转换为B类
operator B()const{}
};
//第二种方法
class A;
class B{
public:
B(const A& a){};//在B类中通过构造函数,将A类型转换成类型B;
};
class A{
public:
//从A类转换为B类
//operator B()const{}
};
#include<iostream>
using namespace std;
class A;
class Fraction{
public:
Fraction(int a,int b = 1):a(a),b(b){}
operator int() const {
return a/b;
}
explicit operator float()const{
return ((float)a)/b;
}
int get(){
return a;
}
//operator A();
private:
int a;
int b;
};
class A{
public:
A(int num):num(num){}
A(Fraction& other){
num = other.get();
}
int Get(){
return num;
}
private:
int num;
};
// Fraction::operator A(){
// return A(a);
// }
class B{
public:
void UseInt(int val){
cout<<val<<endl;
}
void UseFloat(float val){
cout<<val<<endl;
}
};
void F(A a){
cout<<a.Get()<<endl;
}
int main(){
Fraction fra(3,2);
B b;
b.UseFloat(fra);
b.UseInt(fra);
F(fra);
system("pause");
return 0;
}
名字空间的引入
加上名字空间就可以防止在多个文件链接时出现重定义的问题
名字空间;
::cont:全局名字空间下的cout
前面啥也没有代表是当前作用域下的名字
名字空间可以分开实现
#include <iostream>
using namespace std;
//名字空间可以在不同文件中实现一个名字空间
namespace my{
int abc = 100;
class T{ };
namespace my1{
int efg = 200;
class T2{ };
}
}
namespace my{
void myFunc(){
cout<<"show string"<<endl;
}
}
int main(){
namespace me = my;//给my名字空间起别名
my::myFunc();
cout<<me::abc<<endl;
cout<<me::my1::efg;
return 0;
}
匿名名字空间
说明:static int abc = 100
是一个文件级变量,在不同文件中的abc并不是一个abc
因此第一个输出是200;(通过f()函数修改了m1.cpp文件中的abc)
第二个输出是100;(m2.cpp文件中的abc);
现代C++中使用第二种,匿名名字空间,作用和第一种一样
名字的汇入
#include<iostream>
using namespace std;
namespace first{
int x = 5;
}
namespace second{
double x = 5;
}
int main(){
//第一种方法:添加大括号限定作用域
{
using namespace first
cout<<x<<endl;
}
//第二种方法
using first::x;
cout<<x<<endl;
using second::x;//wrong,重复定义
}
友元和友元类
声明友元函数:
友元函数:
自由函数;friend void Show();
类的成员函数;friend A::b();
友元类:friend class A;
例:
嵌套类
class A{
public:
class B{
public:
void f(){}
};
};
//访问
A::B::f();
使用的意义:
将一个或多个嵌套类封装到一个类中;
在使用多个嵌套类的同时,一定程度上“隐蔽”嵌套类
流
字符流;传递的是字符
endl:第一个操作回车,第二个flush()
类间关系
类间关系的强弱:
强关联:
A.h必须包含B.h才能编译成功,则A和B之间存在强关联
// b.h
#pragma once
class A; // 前置声明
class B {
public:
void f(A* pA);
};
// b.cpp
#include "a.h"
#include "b.h"
void B::f(A* pA) {//外联实现
pA->g();
}
弱关联:
类间的逻辑关系
水平方向:
关联
依赖
关联关系
一般关联
聚集关联
聚合关系: B"has-a"A,但B类不负责A类对象的生存与消亡
组合:B" contain-a"A,B类负责A类对象的生存和消亡
依赖关系
放在主动多变的类中,方便以后扩展
7、类的设计🤬
1、面向对象的三大基本特征
-
封装和信息隐蔽
- 封装:通过对客观事物的抽象,分析事物的本质特征,总结和提炼事物的行为和属性,并用类和对象表示的过程
- 信息隐蔽:在用类和对象表示事物是,只将行为和属性公开给可行的外部事物。相应地,隐蔽自身内部特征信息
-
继承
-
多态
2、多个类的设计(存在相互联系)
- 类的拆分
- 联系是否紧密
- 类的合并
- 数据关系
- 将数据拆分成类
- 行为关系
- 依赖关系:
将多个类的行为放在多变复杂的类里,
左图:即将B,C类的函数放在了A类里
3、单个类的设计
-
行为的可见性
- 蓝色字体,即对外不可见;行为参数的抽象与封装
-
行为参数的抽象与封装
-
依赖和关联
-
实现的抽象与封装
- 将函数的实现委托给另一个类来实现,也就是通常说的委托(delegate);
左图:将狗的func函数委托给MyObj类来实现
- 数据的抽象与封装
创建方法与构造函数
创建方法:使用静态创建方法,语意清晰,相比于多个重载函数好理解
4、单件的实现(Singleton设计模式)
单例(Singleton)是一种设计模式,它确保一个类只有一个实例,并提供一个全局访问点来访问该实例。单例模式具有以下优点:
- 全局访问点:单例模式提供了一个全局访问点,使得其他对象可以方便地访问单例对象。这在需要共享资源或提供统一的接口时非常有用。
- 节省资源:由于单例模式只创建一个实例,因此可以节省系统资源。如果多个对象需要共享相同的资源,使用单例可以避免重复创建和销毁对象,提高系统的性能和效率。
- 数据共享:单例模式可以确保多个对象之间共享相同的数据。这对于需要在不同部分之间共享状态或数据的情况非常有用,可以避免数据不一致或冲突的问题。
- 控制实例化过程:单例模式可以对实例化过程进行严格控制,确保始终只有一个实例存在。这对于管理资源、控制并发访问等场景非常有用。
- 惰性实例化:单例模式可以延迟实例化,只在需要时才创建对象。这种惰性实例化的特性可以提高程序的启动速度,并节省内存空间。
- 可扩展性:由于单例模式将实例化过程集中在一个类中,可以相对容易地扩展和修改该类。这使得单例模式更具灵活性和可维护性。
尽管单例模式具有上述优点,但也需要谨慎使用。滥用单例模式可能导致全局状态的滥用、单例对象的耦合性增加等问题。因此,在使用单例模式时,应该根据具体需求和设计考虑是否适合使用,并遵循良好的设计原则和最佳实践。
建议使用指针的方法(指针方式占用内存少)
注意事项:
-
因为单例类的构造函数和析构函数声明为私有(private),因此限制了类的实例化。这样,其他类无法直接实例化该单例类。
-
派生类可以访问基类的公有和受保护成员,所以派生类仍然可以使用基类的公有和受保护成员函数和数据。但无法通过直接实例化派生类来获得单例对象。
-
class Singleton { public: static Singleton& GetInstance() { static Singleton instance; return instance; } Singleton(const Singleton&) = delete; // 禁用拷贝构造函数 Singleton& operator=(const Singleton&) = delete; // 禁用赋值运算符 private: Singleton() {} // 私有构造函数 ~Singleton() {} // 私有析构函数 }; class Derived : public Singleton { // 可以继承Singleton类,但无法直接实例化 }; int main() { // Singleton instance; // 编译错误,无法实例化Singleton类 Singleton& singleton = Singleton::GetInstance(); // 使用单例类的实例 return 0; }
在上述示例中,
Singleton
类的构造函数和析构函数都被声明为私有。通过静态的GetInstance
方法获取Singleton
类的唯一实例。其他类可以继承Singleton
类,但无法直接实例化,因为构造函数是私有的。
8、继承😊
1、类的复用
-
黑盒复用:水平关系—普通关联,聚合、组合和依赖
-
白盒复用:垂直关系—继承
黑盒复用
是一种功能复用,改变被复用类(A)的具体实现,不影响复用类(B,C)的实现
但要求被复用类具有良好的设计(行为的设计合理,独立)
复用类只要有被复用类(A)的定义即可,不需要A的完整原码
使用情况:
白盒复用
白盒复用是一种实现复用或代码复用
2、继承
语法格式:
class <派生类名>:<继承方式><基类名称>
//不写继承方式,默认是private
父类——基类,子类——派生类,
1、派生类:
-
派生类的成员
-
派生类的构造,析构,拷贝,赋值函数
-
派生类中定义的成员函数,数据成员
-
基类中的所有成员(除基类的构造,析构,拷贝,赋值函数,自动转换函数)
-
-
派生类对象的大小
-
派生类中成员的访问控制
派生类的大小包括两部分:上面是继承的基类,和基类一样大小,下面是派生类自己的
注:
基类中的私有成员,派生类不可访问,public继承下,是啥访问控制还是啥访问控制,protected下都是protected
**问题:**既然基类中private的成员在派生类中都是不可访问的,为什么派生类还要含有这些不可访问的基类数据成员和成员函数?
基类的可访问成员函数,可能会使用这些不可访问的基类数据成员和成员函数
保护,可以沿派生一直延续到最后,一直都是保护(除了private)
派生类的构造和析构
基类的构造,拷贝,赋值,自动转换函数不会被派生类自动继承
-
派生类的构造函数:
- 构造顺序:先基类,再派生类
- 初始化列表中可以指定基类的构造函数或拷贝构造函数
- 多重继承时,基类按先后顺序构造(按继承列表)
-
派生类的析构函数
- 先执行派生类的析构,在自行执行基类的析构
newdefine ,redefine,overload,overwrite(hide)
-
newdefine: 派生类中新定义的函数(基类中无同名函数)
-
redefine:派生类中新定义的函数(基类中有同名,同类型的函数)
-
overload: 派生类中多个同名的重载函数
-
overwrite:派生类中定义了某个函数,且基类中有同名的函数,则派生类的函数会将基类的同名函数隐藏(hide)掉。**(只要重名就会被hide)**重载的情况会比较多
- 可以使用using 可以避免隐藏
-
override: 若基类中的某个虚函数(带virtual关键字),并在派生类中定义了同名的函数,称为override
3、继承的选择
4、继承的含义
public继承方式
方便之处
当需要一个car时,实际上给的是car,mtcar,atcar都是可以的,只有公有继承才有这种方式
private继承方式
protected继承方式
5、组合和继承的选择
9、继承和类型转换
protected/private继承下
向下类型转换
- 无实际意义(行为集不相关,数据不全)
- 若确实需要,可在派生类中定义构造函数-- Derived::Deived(const Base& );
向上类型转换(从派生类向基类转换)
- Base类和Derived类中的public互不相同
- 向上类型转换无意义
- 需要,可以强制类型转换或自动转换函数进行转换
- 基类和子类在保护和私有下,完全没有关系,因此向上转换不合理
public继承下的类型转换
向上类型转换
-
1.此时的向上类型转换有意义
-
2.逻辑上,是类型的泛化或一般化
-
3.语言上,public行为集被窄化
向下类型转换
-
1.此时的向下类型转换有意义
-
2.但可能成功,也可能失败
-
3.只能在运行时确定(dynamic_cast)
转换方式
-
1.内置类型的自动转换,如 int->float
-
2.构造函数转换
-
3.定义自动转换函数
-
4.public继承下的向上类型自动转换
-
5.使用类型转换操作符
-
1.static_cast转换操作符
-
2.const_case转换操作符
-
3.reinterpret_cast转换操作符
-
4.dynamic_cast转换操作符
-
10、多重继承
多重继承下的名字冲突问题
使用using并没有解决冲突
名字不重复的菱形结构
解决方案:
虚基类:虚基类延迟创建
其他解决方案
11、虚机制
1、虚机制的引入
静态编联和动态编联
静态编联:(早绑定,静态绑定)
-
编译期间就决定了程序运行时将具体调用那个函数体,即使没有主程序,也能知道程序中各个函数体之间的调用关系
动态编联:
在运行期间,决定具体调用那个函数体
- 动态编联的实现
- 多种方式
- 虚机制(使用虚拟函数和虚拟函数表)
2、使用虚函数
在函数前加一个关键字virtual
-
虚函数的格式(必须是非静态成员函数)
- 声明:
virtual
返回类型 函数名(参数列表)[const]; - 定义: 同一般成员函数
- 声明:
-
虚函数说明
- 必须是成员函数
- 静态成员函数和构造函数,拷贝构造函数不能是虚的
- 析构函数可以是虚函数
- 若类中有其他虚函数,那么析构函数也应该是虚的
- 赋值函数通常不定义为虚的
- 虚函数可以带const修饰,也可以不带
- 访问控制可以任意(public,…)
当基类的析构函数声明为虚函数时,派生类中的析构函数也会被自动声明为虚函数。这样做的目的是为了确保在通过基类指针删除派生类对象时,能够正确调用派生类的析构函数,实现多态的析构。
当使用基类指针指向派生类对象,并通过该指针删除对象时,如果基类的析构函数不是虚函数,那么只会调用基类的析构函数,而不会调用派生类的析构函数。这可能导致派生类中的资源没有正确释放,造成内存泄漏或其他问题。
因此,为了确保在通过基类指针删除派生类对象时能够正确调用派生类的析构函数,应该将基类的析构函数声明为虚函数。这样,当通过基类指针删除对象时,==会先调用派生类的析构函数,然后再调用基类的析构函数,==确保释放所有相关资源。
以下代码发生了overwrite:
#include <iostream>
using namespace std;
class Base {
public:
virtual ~Base() {
std::cout << "Base destructor" << std::endl;
}
virtual void f(){cout<<"Base::f()"<<endl;}
};
class Derived : public Base {
public:
~Derived() override {
std::cout << "Derived destructor" << std::endl;
}
virtual void f(){cout<<"Derived::f()"<<endl;}
};
int main() {
Base* ptr = new Derived();
ptr->f();
delete ptr; // 通过基类指针删除派生类对象,会调用派生类的析构函数
system("pause");
return 0;
}
3、虚拟表
虚函数表:
- 一个指针数组,各元素存放对应虚函数的入口地址
说明:
-
要求对应的类中至少有一个虚函数
-
一个类至多有一个虚拟表,同一个类的不同对象共享该虚拟表
-
首次创建该类实例对象时,在内存中同时创建该类的虚拟表
-
按照函数顺序的序号依次存放入口地址
int main( ) {
Rectangle rect(1,2);
Shape& rRect = rect;
Shape * pCircle = new Circle(3);
rRect.Show( );
//正确输出2
pCircle->Show( );
//正确输出28.26
delete pCircle;
}
4、变量的静态类型和动态类型
静态类型:
在编译期间,可以确定的变量, 如: Child child;
- 指针型:
Parent* pObj = &child
; - 引用类型:
Parent& obj = child
; - 对象类型:
Parent obj = child;
,对象型中的obj的静态和动态一致
动态类型:
在运行时才能确定的,对应于变量的真实类型
int main( ) {
Rectangle rect(1,2);//动态和静态一致,都是Rectangle
Shape& sh1 = rect;//静态时Shape& 动态是Rectangle
Shape * psh2 = new Circle(3);//静态时shape* 动态是 Circle*
rect.Show( );
sh1.Show( );
//正确输出2
psh2 ->Show( );
//正确输出28.26
delete psh2;
void userFunc( const Shape & );
userFunc( rect );
}
void userFunc( const Shape & sh ) {
sh.Show( );
.....
}
编译过程(以p->Func()
为例)
-
确定p的静态类型,假设为
A*
-
在A类中寻找名字叫
Func
,并且参数匹配的函数, -
找不到编译错误
-
找到,该函数是
virtual
函数吗- 不是,编译
p->A::Func()
- 是,采用动态编联,从而编译成
(*p->vptr)[index]((void*)p,..);
,在运行时,根据vptr
中的函数入口地址,选择执行函数
- 不是,编译
-
若希望
p->Func( );
或obj.Func( );
合法,必须有:-
pObj / obj
的静态类型中必须有匹配的函数Func;
,即在A类中有Func()
函数 -
即使匹配的
Func
,永远不被调用,也要有
-
表达式 (*p->vptr)[index]((void *)p, ...)
是使用动态绑定(dynamic dispatch)的一种常见方式来调用虚函数。
-
p->vptr
:通过指针p
访问对象的虚函数表指针。 -
(*p->vptr)
:解引用虚函数表指针,得到虚函数表本身。 -
(*p->vptr)[index]
:通过索引index
访问虚函数表中的某个虚函数的地址。 -
(*p->vptr)[index]((void *)p, ...)
:通过函数指针调用虚函数,传递参数(void *)p, ...
。 -
(void *)p
表达式中的(void *)
是将指针p
转换为void
类型的指针。在这种上下文中,**将p
转换为void
指针的目的通常是为了隐藏具体的对象类型,而只关注函数调用的目标函数。**在虚函数的调用中,编译器将隐含地将this
指针作为第一个参数传递给虚函数。因此,当调用虚函数时,可以将this
指针作为额外的参数传递给虚函数,并将其转换为void
指针类型,以与函数调用的参数匹配。(void *)p
是一种类型转换,它会丢失对象的具体类型信息。在实际的代码中,可能会使用更具体的类型转换,而不仅仅是将this
指针转换为void
指针。这取决于具体的代码实现和上下文。
例子:
注:
对于基类中有虚函数,但是子类并没有与之对应,调用的时候调用基类中的(上图B类中的虚拟表&A::g()
)
静态成员函数与虚函数
静态成员函数不能调用虚函数:
解释:
在C++中,静态成员函数是属于类的函数,而不是属于类的对象或实例的函数。由于静态成员函数与任何特定的对象实例无关,它们无法使用动态绑定(动态多态性)来调用虚函数。
虚函数是通过指针或引用来实现动态绑定的,它在运行时根据对象的实际类型来确定调用哪个函数实现。然而,静态成员函数没有隐式的 this 指针,因此无法进行动态绑定,也无法通过静态成员函数来调用虚函数。
5、虚函数的访问
-
虚函数中访问的非虚函数
- 静态编联,使用本地版本
-
非虚函数中访问的虚函数
- 动态编联
-
虚函数中访问的虚函数
- 动态编联
-
构造函数和虚函数
-
构造函数不能为虚函数
-
调用的虚函数采用静态编联,使用本地版本
-
-
析构函数和虚函数
-
析构函数可以为虚函数
-
若类中含有虚函数,那么析构函数也应为虚函数.
-
调用的虚函数采用静态编联,使用本地版本
-
例
(A表示基类,B表示子类)
A* p = new B;//先调用A的构造函数,访问A::vf(),后调用B的构造函数,访问B::vf();
p->vf();//访问B::vf();
p->vg();//访问A::vg();访问B::vf();访问A::nvh();
p->nvh();//访问B::nvh();访问B::vf();
delete p;//先调用B类中的析构函数,访问B::vf();后调用A类中的析构函数,访问A::vf();
6、私有的虚函数
在 C++ 中,私有的虚函数是无法直接从外部访问的。私有成员只能在类内部访问,无法在类外部或派生类中直接调用。
虚函数的访问性取决于它在类中的访问修饰符。如果一个虚函数被声明为私有(private),那么它只能在类的成员函数内部被调用,而无法从外部通过对象或指针进行访问。
然而,==派生类仍然可以通过继承和访问控制符来重写私有虚函数。==如果一个派生类继承了基类的私有虚函数,并在派生类中重新实现了该函数,那么派生类的成员函数可以调用该虚函数。
总结起来,私有的虚函数只能在类的成员函数内部访问,无法从外部直接访问。派生类可以通过继承和访问控制符来重写私有虚函数,并在派生类的成员函数中访问和调用它。
意思就是说,只有基类类内函数可以调用该虚函数,类外不可以直接调用该虚函数
#include <iostream>
class Base {
private:
virtual void privateVirtualFunction() {
std::cout << "Base::privateVirtualFunction()" << std::endl;
}
public:
void publicFunction() {
privateVirtualFunction(); // 在基类的公有成员函数中调用私有虚函数
}
};
class Derived : public Base {
private://也可以是public
void privateVirtualFunction() override {
std::cout << "Derived::privateVirtualFunction()" << std::endl;
}
};
int main() {
//Derived derived;
Base* p = new Derived;
p->publicFunction(); // 输出: Derived::privateVirtualFunction()
system("pause");
return 0;
}
7、具体类和抽象类
-
具体类:可以实例化
-
抽象类:为子类提供更高层次的抽象,本身不能被实例化,但后裔类可以实例化.
-
抽象类的定义
-
含有一个或多个纯虚函数。
-
纯虚函数格式 (一定是成员函数) :
virtual ReturnType Func(…. ) [const] = 0;
-
纯虚函数的访问控制可任意
-
具体类的子类可以是具体类或抽象类
-
抽象类的子类可以是具体类或抽象类
-
纯抽象类:除静态、构造、析构等函数均为纯虚函数.
-
纯虚定义:对纯虚函数给出缺省实现(定义)
-
-
C++中的接口类
- 是纯抽象类,通常均为public成员,且没有任何非静态的数据成员。
- 抽象类不能实例化是指无法直接创建抽象类的对象,因为抽象类中存在纯虚函数,需要通过派生类来提供具体的实现。抽象类的主要作用是作为接口或基类,定义通用的行为和规范。
8、RTTI(Run Time Type Indentify)和typeid、dynamic_cast操作
12、多态性及其应用
多态性:相同的消息请求,执行不同的代码体,从而有不同的行为后果。
静态多态: 根据目标对象的静态类型和参数表中参数的静态类型确定目标代码体。
-
模板-不同的模板参数
-
函数重载
动态多态
1、静态多态
1、模版
语法:
template<typename T>
函数声明或定义
解释:
template — 声明创建模板
typename — 表面其后面的符号是一种数据类型,可以用class代替
T — 通用的数据类型,名称可以替换,通常为大写字母
注意:
- 函数模板利用关键字 template
- 使用函数模板有两种方式:自动类型推导、显示指定类型
- 模板的目的是为了提高复用性,将类型参数化
2.函数重载
2、动态多态
-
根据目标对象的动态类型和参数表中参数的静态类型确定目标代码体。(虚机制)
-
根据目标对象的动态类型和参数表中参数的动态类型确定目标代码体。(C++不支持)。
class B;
class A
{
public:
virtual ~A( ) { }
virtual void f(A *) { cout<<1<<endl;}
virtual void f(B *) { cout<<2<<endl; }
};
class B:public A
{
public:
virtual ~B( ){ }
virtual void f(A *) { cout<<3<<endl;}
virtual void f(B *) { cout<<4<<endl;}
};
int main()
{
B b;
A*pa = &b;
pa->f(&b);//先找A::f(B*),因为是虚函数,调用B::f(B*) ,输出是4
pa->f(pa);//先找A::f(A*)(因为pa的类型是A*),因为虚函数,调用B::f(A*),输出3
b.f(pa);//调用B::f(A*) 输出是3
return 0;
}
3、虚机制的作用
- 虚机制是实现动态多态的一种方法;
- 意义——在保持客户端访问接口不变的前提下,可以实现变更类的实现
4、子类型化和适应变化
Parent * p = new Child1;
p->Func( );
delete p;
Child2 myObj;
Parent& obj = myObj;
obj.Func( );
void Proc(Parent * p) //更多的以这种形式
{ p->Func(); }
子类型化**(Subtyping)**是面向对象编程中的一个重要概念,它指的是一个类型(子类型)可以被视为另一个类型(父类型)的替代品。子类型化关系体现了“是一个”(is-a)的关系,即子类型是父类型的特殊形式。
子类型化的一个关键概念是Liskov替换原则(Liskov Substitution Principle,LSP),它指出,如果S是T的子类型,则可以在任何需要T类型的地方使用S类型的对象,而不会引发错误或违反预期行为。
子类型化的一个重要应用是多态性(Polymorphism),通过子类型化关系,可以**实现基于父类型的抽象编程,使得代码更加灵活和可扩展。**多态性允许我们在编译时不确定具体对象的类型,而在运行时根据对象的实际类型来调用相应的方法。
适应变化是指在软件开发中,我们需要应对需求的变化、系统的演化或新功能的添加等情况。通过使用子类型化和多态性,我们可以更容易地适应这些变化。
当需求发生变化时,我们可以通过添加新的子类型来扩展系统的功能,而不需要修改现有的代码。由于子类型化的特性,新的子类型可以替代父类型在现有代码中的使用,而无需对现有代码进行修改。
此外,通过多态性,我们可以将具体的实现细节隐藏在父类型的抽象接口背后。当系统需要发生变化时,我们只需要关注接口的定义和父类型的使用,而不需要关心具体子类型的实现细节。这种分离使得代码更加灵活、可维护和可扩展。
因此,子类型化和适应变化是面向对象编程中的重要概念和实践,它们帮助我们构建灵活、可扩展的软件系统,并应对需求的变化和未来的演化。
5、虚拟的拷贝构造
因为构造函数不能虚拟化,因此要构造一种虚拟的拷贝构造
目的:
虚拟(或称为动态)拷贝构造函数是指用于创建对象副本的特殊成员函数。通过使用虚拟拷贝构造函数,可以在运行时根据对象的实际类型来创建正确类型的副本,实现多态性和对象的深拷贝。
虚拟拷贝构造函数通常与多态性和继承关系一起使用。它们允许从基类指针或引用调用拷贝构造函数时,实际上**调用派生类中适当的拷贝构造函数。**这样可以确保在拷贝对象时,复制的是对象的实际类型的数据,而不仅仅是基类的数据部分。
#include <iostream>
class Base {
public:
virtual Base* clone() const {
return new Base(*this);
}
virtual void print() const {
std::cout << "Base" << std::endl;
}
};
class Derived : public Base {
public:
virtual Derived* clone() const override {
return new Derived(*this);
}
virtual void print() const override {
std::cout << "Derived" << std::endl;
}
};
int main() {
Base* basePtr = new Derived();
Base* clonePtr = basePtr->clone();
/*执行过程
basePtr是一个父类指针,指向子类,在调用过程中,因为父类中的clone函数是虚函数,因此调用子类中的clone函数
一般情况下,会将有虚拟拷贝构造函数的构造函数私有化
*/
clonePtr->print(); // 输出: Derived
delete basePtr;
delete clonePtr;
return 0;
}
6、使用继承和虚机制的不足
使用关联,依赖+继承解决子类过多的问题
下面的代码只需要派生出9(2+3+4)中子类
13、面向对象程序设计
1、面向对象程序设计
过程:
-
建立模型
- 用依赖,关联,组合,聚合关系建立较高层次的关系模型
- 根据问题域,领域知识,经验等抽象出类型
- 只使用水平关系
- 主要考察类的公有行为
-
细化模型
- 用依赖,关联,组合,聚合关系降低类型的抽象层次
- 更体现领域知识,经验等的作用
- 只使用水平关系
- 主要考察类的行为
-
类型的抽象与表示
- 用类表示抽象出的类型
- 涉及类的拆分,类的合并,行为的表示,数据的表示和组织等
-
封装变化
- 将可能变化的部分,用类单独封装
- 涉及类的行为接口变化、行为实现的变化、数据表示变化、数据组织的变化等
- 需要领域知识、经验等
- 使用水平关系
-
用子类型化适应变化
- 针对一个维度的变化,用子类型化适应未来变化;
- 对于多个维度的变化,先将各维度的变化独立出来(用水平关系);
-
上述过程迭代
#include <iostream>
class ImpF;
class ImpH;
class Parent {
public:
Parent(ImpF* a, ImpH* b);
virtual ~Parent();
void F();
void H();
private:
ImpF* pF;
ImpH* pH;
};
class ImpF {
public:
virtual ~ImpF() {}
virtual void F() { std::cout << "ImpF::F()" << std::endl; }
};
class ImpH {
public:
virtual ~ImpH() {}
virtual void H() { std::cout << "ImpH::H()" << std::endl; }
};
class A : public ImpF {
public:
void F() { std::cout << "A::F()" << std::endl; }
};
class B : public ImpH {
public:
void H() { std::cout << "B::H()" << std::endl; }
};
//由于 Parent 类的构造函数参数 ImpF* a 和 ImpH* b 是在 Parent 类的定义之前声明的,因此编译器无法识别它们。
//将 Parent 类的构造函数的定义移动到 ImpF 和 ImpH 类定义的后面,并在 Parent 类的定义之前添加适当的类声明
Parent::Parent(ImpF* a, ImpH* b) : pF(a), pH(b) {}
Parent::~Parent() {}
void Parent::F() { pF->F(); }
void Parent::H() { pH->H(); }
int main() {
A a;
B b;
Parent p(&a, &b);
p.F();//输出A::F();
p.H();//输出B::H();
system("pause");
return 0;
}
2、依赖+继承
以老鼠吃水果为例
code:
#include<bits/stdc++.h>
using namespace std;
//水果类
class Fruit{
public:
virtual ~Fruit(){}
//重量
virtual int Weight()const = 0;
//可用部分的百分比
virtual float Precent()const = 0;
};
//老鼠类
class Mouse{
public:
Mouse(float w): weight(w){}
virtual ~Mouse(){ }
virtual void Eat(Fruit& fruit){
weight += fruit.Weight()*fruit.Precent();
}
int getWeight()const {
cout<<weight<<endl;
return weight;
}
protected:
float weight;
};
//水果的子类
//苹果类
class Apple: public Fruit{
public:
Apple(int w): weight(w){}
virtual int Weight()const {return weight;}
virtual float Precent() const {return 0.8;}
private:
int weight;
};
//橘子类
class Orange: public Fruit{
public:
Orange(int w1,int w2): weight1(w1),weight2(w2){}
virtual int Weight()const {return weight2;}
virtual float Precent() const {return 0.7;}
private:
int weight1;
int weight2;
};
//老鼠的子类
//大老鼠
class BigMouse: public Mouse{
public:
BigMouse(float w,float fac):Mouse(w),factor(fac){}
virtual void Eat(Fruit& fruit){
weight += fruit.Weight()*fruit.Precent()*factor;
}
private:
float factor;
};
//小老鼠
class LittleMouse : public Mouse{
public:
LittleMouse(float w,float fac):Mouse(w),factor(fac){}
virtual void Eat(Fruit& fruit){
weight += fruit.Weight()*fruit.Precent()*factor;
}
private:
float factor;
};
int main(){
Apple apple(100);
Orange orange(20,50);
//BigMouse mouse(120,0.5);
Mouse* mouse = new BigMouse(120,0.5);
mouse->Eat(apple);
mouse->getWeight();//输出160
mouse->Eat(orange);
mouse->getWeight();//输出177.5
system("pause");
return 0;
}
//第二种实现方式,明显优于第一种方法
//老鼠类
class Mouse{
public:
Mouse(float w): weight(w){}
virtual ~Mouse(){ }
virtual void Eat(Fruit& fruit){
weight += fruit.Weight()*fruit.Precent()*Factor();
}
int getWeight()const {
cout<<weight<<endl;
return weight;
}
virtual float Factor() = 0;
protected:
float weight;
};
//大老鼠
class BigMouse: public Mouse{
public:
BigMouse(float w,float fac):Mouse(w),factor(fac){}
// virtual void Eat(Fruit& fruit){
// weight += fruit.Weight()*fruit.Precent()*factor;
// }
float Factor(){return factor;}
private:
float factor;
};
方案3(双向依赖)变换3
class Mouse;
//水果类
class Fruit{
public:
virtual ~Fruit(){}
//重量
virtual int Weight()const = 0;
//可用部分的百分比
virtual float Precent(Mouse& mouse)const = 0;
};
//老鼠类
class Mouse{
public:
Mouse(float w): weight(w){}
virtual ~Mouse(){ }
virtual void Eat(Fruit& fruit){
weight += fruit.Weight()*fruit.Precent(*this)*Factor();
}
int getWeight()const {
cout<<weight<<endl;
return weight;
}
virtual float Factor() = 0;
protected:
float weight;
};
//水果的子类
//苹果类
class Apple: public Fruit{
public:
Apple(int w): weight(w){}
virtual int Weight()const {return weight;}
virtual float Precent(Mouse& m) const {return 0.8+ (m.getWeight()>=80?0.015:0.02);}
private:
int weight;
};
变化5
//Monster.Dog.Cat没有kill接口
Crocodile有Kill接口
bool Crocodile::kill(Monster & other) {
…
}
#include <iostream>
using namespace std;
class Monster {
public:
string name;
Monster(string name) : name(name) {}
virtual void makeSound() = 0;
};
class Dog : public Monster {
public:
Dog(string name) : Monster(name) {}
void makeSound() override {
cout << "Woof!" << endl;
}
};
class Cat : public Monster {
public:
Cat(string name) : Monster(name) {}
void makeSound() override {
cout << "Meow!" << endl;
}
};
class Crocodile : public Monster {
public:
Crocodile(string name) : Monster(name) {}
void makeSound() override {
cout << "Roar!" << endl;
}
void kill() {
cout << "Crocodile attacks and kills its prey!" << endl;
}
};
int main() {
Dog dog("Fido");
Cat cat("Whiskers");
Crocodile crocodile("Snappy");
Monster* p = new Crocodile("ll");
dog.makeSound(); // 输出: Woof!
cat.makeSound(); // 输出: Meow!
crocodile.makeSound(); // 输出: Roar!
crocodile.kill(); // 输出: Crocodile attacks and kills its prey!
// 将指针 p 转换为 Crocodile* 类型
Crocodile* crocodilePtr = dynamic_cast<Crocodile*>(p);
if (crocodilePtr) {
// 转换成功,调用 Crocodile 类的 kill() 函数
crocodilePtr->kill();
} else {
// 转换失败,指针 p 不指向 Crocodile 对象
cout << "Error: p does not point to a Crocodile object." << endl;
}
delete p; // 记得释放内存
system("pause");
return 0;
}
动态类型转换的使用
要通过指向基类的指针调用派生类的成员函数,需要将指针进行类型转换,以确保调用的是正确的函数。
上述代码中,将p指针转换成子类的类型,使用dynamic_cast
请注意,使用dynamic_cast
进行指针类型转换需要注意以下几点:
- 基类中必须至少有一个虚函数,以使得类型转换能够正确进行。
- 在进行类型转换时,如果指针指向的对象类型与转换的目标类型不兼容,转换结果将是一个空指针(nullptr)。
3、关联+继承
1、基本型—没有区分普通关联、组合、聚合
例1
class Police {
public:
bool Trace(Bandit & bandit, int hours) {
return vieche->getMaxSpeed( ) > bandit.Speed();
}
private:
Vieche* vieche;
};
Example 2(委托,代理)
class Scientist {
public:
float CircleArea(float r ) {
return computer->caculateArea(r);
}
private:
Computer * computer;
};
对于一对一的情况,还可以表示客户类中某个函数的功能,全部或部分委托给其他对象
Example 3 (组合)
class Bucket {
public:
Bucket() {
for(int i=0;i<5;++i) fruit[i] = new Apple;
for(int i=5;i<10;++i) fruit[i] = new Orange;
}
~Bucket()
{ for(int i=0;i<10;++i) delete fruit[i]; }
private:
Fruit * fruit[10];
};
Bucket
负责水果类的生存和消亡
变化1
class XApp {
public:
virtual ~XApp() {}
virtual void XFunc() = 0;
};
class AbstractApp {
public:
virtual ~AbstractApp() {}
virtual void Func( ) = 0;//通过Func()函数
};
class WebApp: public AbstractApp {
public:
WebApp(XApp & x):xApp(x) { }
virtual void Func( ) //override AbstractApp::Func();
{ xApp.XFunc( ); }//调用其成员 xApp::XFunc();
private:
XApp & xApp;//关联
};
变化2
变化3
面向对象设计原则
软件的可维护性和可复用性
- 过于僵化:设计难以修改
- 过于脆弱:设计易遭到破坏
- 牢固性:复用性低
- 黏度过高:难以做正确的事
好的系统设计:
- 可扩展性
- 灵活性
- 可插入性
单一职责原则(4星重要)
开闭原则(5星重要)
对扩展开放,对修改关闭
抽象化是实现开闭原则的关键
对可变性封装原则要求对可变的部分进行封装
例题
li2
里氏替换(四星)
依赖倒置原则
简单来讲,依赖倒置原则是指:代码要依赖于抽象的类,而不是依赖于具体的类,要针对接口或抽象类编程,而不是针对具体的类编程
依赖注入:
例题:
依赖于抽象的类,抽象类起到了隔离的作用
接口隔离原则
迪米特法则
一个软件实体应当尽可能少的与其他实体发生相互作用
A通过B调用C
合成复用原则
对于继承复用:父类发生改变,子类都受牵连
小结
本书完|
C++中杂知识点:
- 既然每个类的成员函数都是确定的,为什么还通过建立虚表实现多态?
- 在C++中,虚函数和虚表的机制提供了运行时多态性的实现。虚函数允许在派生类中重写基类的函数,并且通过基类指针或引用调用这些函数时,会根据实际对象的类型来动态地确定要调用的函数实现。
- 通过建立虚表(也称为虚函数表),编译器在运行时能够通过对象的指针或引用来查找正确的虚函数实现。每个具有虚函数的类都有一个虚表,其中包含了该类的虚函数的地址。
- 总结起来,通过建立虚表和使用虚函数,C++实现了运行时多态性,使得在基类指针或引用下能够处理派生类对象,并根据对象的实际类型动态地调用正确的函数实现。这是面向对象编程的重要特性之一。
运算符重载的选择
1,一般,单目运算符最好重载为类的成员函数,双目运算符最好重载为类的友元函数。
2,若一个运算符的操作需要修改对象的状态,选择重载为成员函数较好。
3,若运算符的操作数(尤其是第一个操作数)可能有隐式类型转换,则只能选用友元函 数。
4,具有对称性的运算符可能转换任意一端的运算对象,如:算术(a+b和b+a)、关系运算 符(a>b和b<a)等,通常重载为友元函数。
5,有4个运算符必须重载为类的成员函数:赋值=、下标[ ]、调用( )、成员访问->。
#include <iostream>
using namespace std;
class A1 {
public:
float x;
A1(float a) : x(a) {}
A1& operator++(){//前置++
x++;
return (*this);
}
A1 operator++(int ){//后置++
A1 temp = *this;
x++;
return temp;
}
friend float operator+(float y,A1 a){
return y+a.x;
}
A1 operator()(int a,int b){//仿函数
return a+b;
}
//自定义赋值函数
};
auto& operator<<(auto& cout,A1 a){//左移运算符和右移运算符必须用全局函数实现
cout<<a.x;
return cout;
}
class B{
public:
B(int x){
a = new int (x);
}
B& operator=(const B& other){
if(this!=&other){
delete a;
a = new int(*other.a);
}
return *this;
}
void get(){
cout<<*a<<endl;
}
private:
int * a;
};
int main() {
// A1 a1(1.2);
// a1++;
// cout<<a1.x<<endl;
// float m = 1.2+a1;
// cout<<m<<endl;
// cout<<a1(1,2);
int a = 10;
B b(a);
b.get();
int c = 100;
B b1(c);
b = c;//可以通过构造函数将c(int类型)转换成(B类型)//隐式类型转换
b.get();
std::cin.get(); // 等待用户按下回车键
return 0;
}
7)]
[外链图片转存中…(img-R9vPyqQb-1703430401607)]
[外链图片转存中…(img-aQ20iv8r-1703430401607)]
依赖倒置原则
[外链图片转存中…(img-cowrJC9e-1703430401608)]
简单来讲,依赖倒置原则是指:代码要依赖于抽象的类,而不是依赖于具体的类,要针对接口或抽象类编程,而不是针对具体的类编程
[外链图片转存中…(img-a0SLOG3X-1703430401608)]
[外链图片转存中…(img-sjyafGhh-1703430401608)]
[外链图片转存中…(img-niiXyCz1-1703430401608)]
依赖注入:
[外链图片转存中…(img-pMoqCtvz-1703430401608)]
例题:
依赖于抽象的类,抽象类起到了隔离的作用
[外链图片转存中…(img-is7TC4ft-1703430401609)]
[外链图片转存中…(img-beAfMB7B-1703430401610)]
接口隔离原则
[外链图片转存中…(img-bDg5DPK0-1703430401610)]
[外链图片转存中…(img-amqSqgzD-1703430401610)]
迪米特法则
一个软件实体应当尽可能少的与其他实体发生相互作用
[外链图片转存中…(img-TCtMbKyv-1703430401610)]
[外链图片转存中…(img-DZ2V8lRD-1703430401611)]
[外链图片转存中…(img-sFzSAPoR-1703430401611)]
[外链图片转存中…(img-67JPjmHm-1703430401611)]
A通过B调用C
[外链图片转存中…(img-YLQuC9FW-1703430401611)]
合成复用原则
[外链图片转存中…(img-TZ2tBOSG-1703430401611)]
[外链图片转存中…(img-P71OAKCH-1703430401612)]
[外链图片转存中…(img-SkRt5meO-1703430401612)]
对于继承复用:父类发生改变,子类都受牵连
[外链图片转存中…(img-ItN3vbG1-1703430401612)]
[外链图片转存中…(img-IAPXsVm5-1703430401612)]
小结
[外链图片转存中…(img-7pGReTx5-1703430401612)]
[外链图片转存中…(img-8wLvM4D4-1703430401613)]
[外链图片转存中…(img-AWS82BqA-1703430401613)]
[外链图片转存中…(img-Z46oToeJ-1703430401613)]
本书完|
C++中杂知识点:
- 既然每个类的成员函数都是确定的,为什么还通过建立虚表实现多态?
- 在C++中,虚函数和虚表的机制提供了运行时多态性的实现。虚函数允许在派生类中重写基类的函数,并且通过基类指针或引用调用这些函数时,会根据实际对象的类型来动态地确定要调用的函数实现。
- 通过建立虚表(也称为虚函数表),编译器在运行时能够通过对象的指针或引用来查找正确的虚函数实现。每个具有虚函数的类都有一个虚表,其中包含了该类的虚函数的地址。
- 总结起来,通过建立虚表和使用虚函数,C++实现了运行时多态性,使得在基类指针或引用下能够处理派生类对象,并根据对象的实际类型动态地调用正确的函数实现。这是面向对象编程的重要特性之一。
运算符重载的选择
1,一般,单目运算符最好重载为类的成员函数,双目运算符最好重载为类的友元函数。
2,若一个运算符的操作需要修改对象的状态,选择重载为成员函数较好。
3,若运算符的操作数(尤其是第一个操作数)可能有隐式类型转换,则只能选用友元函 数。
4,具有对称性的运算符可能转换任意一端的运算对象,如:算术(a+b和b+a)、关系运算 符(a>b和b<a)等,通常重载为友元函数。
5,有4个运算符必须重载为类的成员函数:赋值=、下标[ ]、调用( )、成员访问->。
#include <iostream>
using namespace std;
class A1 {
public:
float x;
A1(float a) : x(a) {}
A1& operator++(){//前置++
x++;
return (*this);
}
A1 operator++(int ){//后置++
A1 temp = *this;
x++;
return temp;
}
friend float operator+(float y,A1 a){
return y+a.x;
}
A1 operator()(int a,int b){//仿函数
return a+b;
}
//自定义赋值函数
};
auto& operator<<(auto& cout,A1 a){//左移运算符和右移运算符必须用全局函数实现
cout<<a.x;
return cout;
}
class B{
public:
B(int x){
a = new int (x);
}
B& operator=(const B& other){
if(this!=&other){
delete a;
a = new int(*other.a);
}
return *this;
}
void get(){
cout<<*a<<endl;
}
private:
int * a;
};
int main() {
// A1 a1(1.2);
// a1++;
// cout<<a1.x<<endl;
// float m = 1.2+a1;
// cout<<m<<endl;
// cout<<a1(1,2);
int a = 10;
B b(a);
b.get();
int c = 100;
B b1(c);
b = c;//可以通过构造函数将c(int类型)转换成(B类型)//隐式类型转换
b.get();
std::cin.get(); // 等待用户按下回车键
return 0;
}