目录
1.基本函数
2.浅拷贝和深拷贝
3.初始化列表
4.const关键字的使用
5.静态成员变量和成员函数
6.C++对象模型
7.友元
8.自动类型转换
9.继承
1.基本函数
(1)构造函数,这个需要注意的就是我们如果使用类名加括号,括号里面加参数的方式这样的写法不是在调用构造函数,而是在创建一个匿名对象;
using namespace std;
class cgril
{
public:string m_name;int m_age;char m_memo[600];cgril(){initdata();cout << "这里调用了cgril()构造函数" << endl;}cgril(string name)//一个姓名参数的初始化函数{initdata();cout << "调用了cgril(string name)构造函数" << endl;m_name = name;}cgril(int age)//一个年龄参数的初始化函数{initdata();cout << "调用了构造函数" << endl;m_age = age;}cgril(string name, int age){initdata();cout << "这里的调用了构造函数" << endl;m_name = name;m_age = age;}void initdata(){m_name.clear();m_age = 0;memset(m_memo, 0, sizeof(m_memo));}void show(){cout << "姓名:" << m_name << ",年龄:" << m_age << ",备注:" << m_memo << endl;}
};
(2)下面的两种写法看似一样,实际上是截然不同的:
第一行和23两行可以实现相同的效果,但是这个第一行是属于显示的调用构造函数,执行一次构造函数和析构函数;
23行可以达到这个效果,但是会执行两次构造函数和析构函数,因为这个cgri("张三",20)执行的时候,会生成一个临时对象,赋值给这个gril对象,相当于是赋值函数,因为有这个临时对象的参与,所以就会执行两次这个构造函数,一次是构造gril这个对象,一次是构造临时对象;
(3)下面的就是两个拷贝构造函数,第一个是没有重载的,第二个是重载的;
cgril(const cgril& gg)
{m_name = gg.m_name;m_age = gg.m_age;cout << "调用没有重载的拷贝构造函数" << endl;
}
cgril(const cgril& gg1,int ii)
{m_name = gg1.m_name;m_age = gg1.m_age - ii;cout << "调用重载的拷贝构造函数" << endl;
}
我们把这个没有重载的函数注释掉,发现这个还是可以实现把g1的内容拷贝给g2的,这个时候他没有去调用这个重载的函数,因为这个重载的函数需要有2个参数,但是我们调用的时候,只传递一个参数 ,如果这个函数的第二个参数存在缺省参数,编译器是会去调用的,但是这个时候没有缺省参数,依然可以实现拷贝,这个就说明如果类里面定义了重载的拷贝构造函数却没有提供这个默认的拷贝构造函数,这个时候编译器会自动提供拷贝构造函数;
cgril g1;
g1.m_name = "张三";
g1.m_age = 18;
cgril g2(g1);
g2.show();
(4)编译器的优化问题,就是返回值如果是一个类的时候,可能会调用拷贝构造函数(这个和变编译器有关),有的编译器就会调用这个拷贝构造函数,有的会把这个步骤给优化掉,直接使用这个对象,只不过给这个对象换了一个名字罢了;
cgril func()
{cgril gg;gg.m_name = "张三";gg.m_age = 18;return gg;
}
int main()
{cgril ggg = func();ggg.show();
}
2.浅拷贝和深拷贝
(1)我们这个编译器默认提供的拷贝构造函数就是一个浅拷贝,如果指针p1指向一块空间,拷贝给指针p2之后,两个指针就会指向同一块空间,释放一个空间另外的一个指针的指向就不存在了,也就是说另外的一个指针就会变成野指针,而且对于这个一个指针数据的修改同样会影响另外的一个指针数据;
(2)深拷贝解决了浅拷贝的弊端,就是如果想拷贝,不是原来的那样直接指向同一块空间,而是开辟出新的空间,把这个原来的空间的内容给释放掉,这样的话两个指针各自操控各自的区域,就不会出现上面的野指针的问题了;
3.初始化列表
(1)基本写法
这个初始化列表是跟在这个构造函数的后面的,如果使用了初始化列表,就不能再这个构造函数里面再次给这个成员变量赋值,因为这个时候构造函数里面的赋值就会覆盖掉原来的初始化列表里面的初始值;
(2)初始化列表里面的数值可以是具体的值,也可以是这个参数,表达式等等;
cgril(string name, int age):m_name(name),m_age(age)
{cout << "这里的调用了构造函数" << endl;
}int main()
{cgril g1("李四", 18);g1.show();return 0;
}
这个是实例里面,我们就是使用这个形参作为初始化列表里面的初始值;
我们还可以在这个初始化列表的这个括号里面添加文字,进行这个表达式的书写,例如,我们可以对于这个年龄进行加减操作,对于这个名字的前面加上一些修饰语,如下所示:
cgril(string name, int age):m_name("高大威猛的" + name), m_age(age + 6)
{cout << "这里的调用了构造函数" << endl;
}
(3)当这个初始化列表的成员是类的时候,使用初始化列表调用的是成员类的拷贝构造函数,而赋值是先创建成员类的对象,这个时候调用的是成员类的普通构造函数,然后再赋值;
下面的这段代码有助于我们了解两者之间的区别:
class cboy
{
public:string m_xm;//男朋友的姓名cboy(){m_xm.clear();cout << "调用cboy()普通构造函数" << endl;}cboy(string name){m_xm = name;cout << "调用只有一个参数的cboy(string name)构造函数" << endl;}cboy(const cboy& bb){m_xm = bb.m_xm;cout << "调用了cboy(const cboy& bb)拷贝构造函数" << endl;}
};
class cgril
{
public:string m_name;int m_age;cboy m_boy;cgril(){cout << "这里调用了cgril()构造函数" << endl;}cgril(string name) //一个姓名参数的初始化函数{cout << "调用了cgril(string name)构造函数" << endl;m_name = name;}cgril(int age) //一个年龄参数的初始化函数{cout << "调用了cgril(int age)构造函数" << endl;m_age = age;}cgril(string name, int age, cboy boy):m_name(name), m_age(age){m_boy.m_xm = boy.m_xm;cout << "调用cgril(string name, int age, cboy boy)构造函数" << endl;}void show(){cout << "姓名:" << m_name << ",年龄:" << m_age<<",男朋友:"<<m_boy.m_xm << endl;}
};
int main()
{cboy boy("张三");cgril gg("李四", 18, boy);gg.show();return 0;
}
这个就是上面的代码的执行情况, 执行主函数里面的第一行的时候就会调用cboy类里面的构造函数,执行主函数里面的第二行的时候,因为这个实参boy是一个对象,传递给形参的时候使用的是值传递,所以这个时候就会执行这个拷贝构造函数,这个构造函数就是实参的内容传递给形参;
用类创建对象的时候,先去初始化构造函数的形参对象,然后再去初始化类的成员;控制台输出的第四行日志是cgril的对象gg创建的时候被调用的,这个很好理解;
当我们把这个boy形参改为引用传参,而不是原来的值传参的时候,这个时候就不会调用这个拷贝构造函数了,因为这个引用的话使用的是和实现相同的东西,只不过是找了一个别名罢了;
上面介绍的所有内容,这个超女的男朋友进行这个初始化列表的时候,并没有走这个初始化列表,相反这个是先执行构造函数,在这个函数体里面进行初始化,这个是两个步骤;
下面我们使用初始化列表对于这个超女类的成员进行初始化操作:
这个时候我们发现当我们使用初始化列表的时候,调用的是成员类的拷贝构造函数,这个时候对象的初始化和赋值是两步操作,使用初始化列表,对象的初始化和赋值就是一步操作;
这个时候对比之前的没有使用初始化列表的情况,显示对于这个形参执行的是普通构造函数,然后在这个构造函数体里面进行赋值,这个时候相信你就可以理解最开始的时候这句话的意思了:
当这个初始化列表的成员是类的时候,使用初始化列表调用的是成员类的拷贝构造函数,而赋值是先创建成员类的对象,这个时候调用的是成员类的普通构造函数,然后再赋值;
可见这个一个类的成员变量是一个类的时候这个初始化列表和普通的赋值初始化有以下的区别:
第一就是这个调用的函数不一样,赋值进行初始化时候,调用的是子类的普通构造函数,然后执行这个函数体里面的赋值语句对于这个成员变量进行赋值;但是这个初始化列表的方法就是调用拷贝构造函数;
第二点就是效率不一样,普通的赋值初始化就是先创建,初始化之后赋值,但是这个初始化列表是初始化和赋值是一步就完成的;(请读者下去慢慢体会两者的区别)
(4)当这个类的成员是常量或者是引用的时候,只能使用初始化列表进行初始化操作;
常量指的就是这个成员变量的前面加上const关键字进行修饰,这个时候的成员变量必须在定义的时候进行初始化,而我们的这个先构造再赋值就不符合这个条件,只能使用初始化列表的方式进行初始化;例如,下面的这两种情况都需要使用初始化列表的方式进行初始化;
4.const关键字的使用
(1)下面我们对于这个const的使用会在下面的这个代码基础上面进行修改和调整;
class cgril
{
public:string m_name;int m_age;cgril(const string& name, int age){m_name = name;m_age = age;cout << "调用两个参数的cgril(const string& name, int age)构造函数" << endl;}void show() const{cout << "姓名:" << m_name << ",年龄:" << m_age << endl;}
};int main()
{cgril g("张三", 18);g.show();return 0;
}
这个代码里面有一个show函数,这个函数的作用就是打印输出这个名字和年龄的值,我们是不会对于这个数值进行修改的,这样的情况下我们就可以在这个函数的后面加上const进行修饰,表示这个函数不会对于这个成员变量的值进行修改;
这个时候我们如果在这个函数里面对于这个数值进行修改,程序就会报错;
(2)函数的相关调用
非const修饰的函数可以调用const修饰的和非const修饰的,但是const修饰的函数只能调用const修饰的函数;
同理,我们对于这个函数可以使用const进行修饰,对于这个对象,我们也可以使用const进行修饰,这样的对象叫做常对象,常对象被这个const修饰,因此常对象只能调用const修饰的函数,不能调用非const修饰的函数;
我们在运行的时候会发现,这个会执行构造函数,但是构造函数没有const修饰,依然可以执行通过,这个时候我们就可以理解为常对象调用const修饰的函数针对的是我们自己定义的函数,这个构造函数编译器也会提供,所以可以调用,如果我们在这个构造函数后面加上const修饰,反而程序会报错,这个是因为在构造函数后面加上const是非法的;
4.this指针
(1)我们通过下面的这段代码逐步过渡到thsi指针,这个代码实现的就是两个超女的颜值比较,然后输出颜值更高的,我们使用yz这个整型变量表示颜值;
class cgril
{
public:string m_name;int m_yz;//这个代表的是颜值,我们使用整形数字代替,1表示颜值最高,3表示颜值最低;cgril(const string& name, int yz){m_name = name;m_yz = yz;}void show() const{cout << "我是" << m_name << ",颜值最高的超女" << endl;}
};
const cgril& pk(const cgril& gg1, const cgril& gg2)
{if (gg1.m_yz < gg2.m_yz){return gg1;}else{return gg2;}
}
int main()
{cgril g1("张三", 1);cgril g2("李四", 2);cgril g3 = pk(g1, g2);g3.show();return 0;
}
(2)这个代码是可以实现颜值比较的功能的,但是他没有显示出来C++的优势,下面我们使用this指针实现以下这个功能;
class cgril
{
public:string m_name;int m_yz;//这个代表的是颜值,我们使用整形数字代替,1表示颜值最高,3表示颜值最低;cgril(const string& name, int yz){m_name = name;m_yz = yz;}void show() const{cout << "我是" << m_name << ",颜值最高的超女" << endl;}const cgril& pk(const cgril& gg) const{if (gg.m_yz < m_yz){return gg;}else{return *this;}}
};
int main()
{cgril g1("张三", 1);cgril g2("李四", 2);const cgril& g3 = g1.pk(g2);g3.show();return 0;
}
这个里面更符合C++的编程逻辑,就是使用thsi指针,这个比较颜值的函数不再放在类的外面,而是放在类的里面作为一个成员函数,这样的成员函数实际上在传参的时候都传递过去了一个this指针,只不过不会显示,谁调用的成员函数,这个this指针就会指向谁,这个地方好像this指针的好处还显示不出来;
我们上面是比较的两个超女的颜值,因此这个C语言风格的比较判断还很简洁,并不是很复杂,但是当我们需要比较五个超女的颜值的时候可能这个C++里面的thsi指针就会更具有优势;
这个时候我们的成员函数不变,但是这样写这个函数的调用就会很能凸显这个this指针的精妙了;
这个时候我们的写法就可以使用这个指针只用一个式子就可以比较这五个的颜值;
5.静态成员变量和成员函数
(1)静态成员变量就是在我们写的这个普通成员变量的前面加上const关键字,普通的成员变量就变成了静态成员变量;
class cgril
{
public:string m_name;static int m_age;cgril(const string& name, int age){m_name = name;m_age = age;}void showname(){cout << "姓名:" << m_name << endl;}void showage(){cout << "年龄:" << m_age << endl;}
};
int cgril::m_age = 0;
int main()
{cgril gg("张三", 18);gg.showname();gg.showage();return 0;
}
(2)上面的程序我们把这个m_age的成员变量定义为静态的成员变量,这个时候我们在这个全局区里面必须对于这个静态的成员变量进行初始化,需要指定这个作用域(实际上就是类名),对于这个普通的成员变量,我们不能在这个主函数里面直接打印输出结果,但是对于这个静态的成员变量,我们可以加上他的作用域之后直接打印输出结果;
这个时候我们就可以发现,这个这个静态成员变量是可以直接打印的,但是对于这个普通的成员变量,如果我们直接打印,就会报错,这个是因为这个普通的成员变量必须使用创建得对象进行调用,我们的静态成员变量是可以直接进行打印输出结果的;
(3)关于静态成员函数
我们上面的静态成员变量可以直接使用,不需要创建对象,同理,如果我们把这个函数设置成成员函数,我们也是可以直接调用的,对于普通的成员函数,我们也是需要创建对象之后使用对象调用函数;
关于这个静态成员变量是放在这个全局区进行定义初始化的,所以这个成员变量的作用就和这个全局变量基本是一样的,只有程序结束的时候这个成员变量才会被销毁;
这个静态成员变量只有一份,所以即使我们对于这个静态的成员变量多次进行赋值,最后的时候这个成员变量的值只会保留一份,但是普通的成员变量的值还是显示不同的,但是这个静态成员变量的值只有一份,所以打印结果显示的静态的成员变量都是相同的数值,因为这个最新的20已经把前面的两个年龄全部覆盖掉了;
(4)静态成员变量函数的访问权限
我们前面使用的这个是把年龄这个成员变量静态化,函数也是把这个打印年龄的成员函数静态化,这个时候我们在静态成员函数里面只能调用;
另外这个静态成员函数没有this指针,在非静态成员函数里面可以访问静态成员变量;
私有的静态成员无法被类外面的成员访问,这个想要验证的话也是很简单的,我们把这个定义的静态的成员变量挪动到public的上面去,这个时候没有范围修饰的静态成员变量的默认属性就是私有的,这个时候我们如果在主函数里面对于这个成员变量进行调用就会报错;(这个也是静态成员变量区别于全局变量的重要原因)
6.C++对象模型
(1)这个存在一个叫做指针表,通过这个指针表就可以找到对象成员的地址,对象成员的地址在物理上面不一定是连续的,但是这个指针表的指针一定是连续的,并且这个指针表的大小是固定的,通过这个指针表我们就可以找到这个;类里面的成员的地址;
(2)静态的成员变量属于类,不属于对象,所以这个静态的成员变量是独特一份的,不会因为这个创建的对象的多少影响到静态的成员变量占用的内存空间的大小;
(3)成员函数也是属于类的,无论这个对象怎么变,这个成员函数都是不受影响的,它的大小也不会计算在这个对象里面,他是独立于对象存储的;
(4)成员函数的地址和成员变量的地址明显不在一起,但是成员函数的地址和这个普通函数的地址是在一块的;
(5)在我们程序员看来这个类里面的东西好像是完整的,实际上这个成员变量和成员函数都是分散存储在这个内存里面的,所以在这个C++内部,一定有一个东西用来记录这个成员的地址,这个东西就是我们上面提及到的指针表;通过这个指针表我们就可以找到这个成员变量和成员函数的地址;
(6)如果成员函数里面没有用到成员变量,这个时候我们可以使用空指针调用这个成员函数,但是如果这个成员函数里面使用到了这个成员变量,我们就需要进行判断这个指针是否为空;
换言之,通俗的讲,就是这个我们在没有创建对象的时候,调用函数,当这个函数里面吗,当这个函数里面没有使用到成员变量的时候,我们是可以对于这个函数进行调用的;
但是当这个函数里面使用了成员变量的时候,而且没有创建对象,我们使用这个成员变量就相当于这个使用空指针,这个时候如果我们直接运行就会报错,这个时候就需要对于这个指针进行判断是否是空指针;
(7)对象的地址是第一个非静态成员变量的地址,如果没有非静态的成员变量,编译器会隐含的增加一个字节作为占位成员;
7.友元
(1)友元实际上就是当我们没有办法访问某个私有成员变量的时候,我们可以把这个函数设置为这个类的友元函数,这样的话我们的函数就可以访问这个类的私有成员变量;
using namespace std;
class cgril;
class cboy
{
public:void func1(const cgril& g);void func2(const cgril& gg);
};class cgril
{friend void cboy::func1(const cgril& g);friend void cboy::func2(const cgril& gg);
public:string m_name;void showname(){cout << "姓名:" << m_name << endl;}cgril(){m_name = "张三";m_age = 18;}
private:int m_age;void showage(){cout << "年龄:" << m_age << endl;}
};void cboy::func1(const cgril& g) { cout << g.m_name << endl; }
void cboy::func2(const cgril& gg) { cout << gg.m_age<< endl; }int main()
{cgril g;cboy b;b.func1(g);b.func2(g);return 0;
}
(2)我们知道的都是对于一个函数使用自己的类里面的某个私有成员,这个时候我们可以把这个函数放到类的里面作为友元函数,这个函数的作用就是让我们可以使用这个类里面的私有成员;
上面的是一个友元函数的情况,涉及到两个类之间的相互调用,这个时候就是函数的声明和定义过程比较繁琐;这个时候我们在这个cboy的类里面就可以调用cgril类里面的东西了;这个友元函数的作用就是我们可以调用其他类里面的函数;
8.自动类型转换
(1)下面的就展示了几种自定义类型数据的转换方法,例如这个显示转换,隐式转换,先创建对象,再构建临时变量,再去赋值;
class cgril
{
public:int m_bh;string m_name;double m_weight;cgril(){m_bh = 0;m_name.clear();m_weight = 0;}void show(){cout << "编号:" << m_bh << "姓名:" << m_name << "体重:" << endl;}cgril(int bh)//只有一个参数的构造函数{m_bh = bh;m_name.clear();m_weight = 0;}
};
int main()
{//常规写法cgril g1(8);//显示转换cgril g1 = (cgril)8;;//隐式转换cgril g1 = 8;//创建对象,创建临时对象,再赋值cgril g1;g1 = 8;g1.show();return 0;
}
(2)这个自动类型转换不一定总是好的,我们如果不想使用自动类型转换,只需要在这个构造函数的前面添加exolcit关键字,就会关闭编译器的自动类型转换功能;
(3)转换函数:这个函数就是把这个类转换为我们的内置数据类型,例如下面的这个例子,我们的类里面有string,int double三种数据类型,我们使用这个转换函数之后就可以进行这个对应的转换:operator 数据类型{}就是这个转换函数的基本格式;
using namespace std;
class cgril
{
public:int m_bh;string m_name;double m_weight;cgril(){m_bh = 6;m_name = "张三";m_weight = 66.6;}operator int() { return m_bh; }operator double() { return m_weight; }operator string() { return m_name; }
};
int main()
{cgril g;int a = g;cout << "a=" << a << endl;string b = g;cout << "b=" << b << endl;double c = g;cout << "c=" << c << endl;return 0;
}
(4)实现转换的其他写法:下面的两种写法也可以达到相同的效果;
9.继承
(1)继承也叫做派生,分为这个基类和派生类,也叫做父类和子类,表达的意思都是一样的,只不过这个站的角度不一样;
(2)为了更好地了解继承的相关语法,我们构建下面的这个场景来进行相关的介绍:我们首先定义了一个allcomers类用来表示的是这个选秀活动参加人员的名字和电话,我们定义的第二个类表示的就是通过海选的超女,这个时候,我们这个通过海选的超女肯定也是需要这个名字和电话这些基本信息的,我们就可以继承allcomers类的相关成员变量,allcomers类就叫做父类,cgril类就叫做子类;
(3)继承就可以理解为子类可以获取这个父类的相关的成员变量和成员函数,我们实例里面使用public作为这个继承的方式,而且在这个子类里面我们是可以添加新的成员变量和成员函数的,如果继承,父类的成员函数和成员变量子类都会有的;
class allcomers
{
public:string m_name;string m_tel;allcomers(){m_name = "某某某";m_tel = "不详";}void sing(){cout << "我是一只小小鸟" << endl;}void setname(const string& name){m_name = name;}void settel(const string& tel){m_tel = tel;}
};
class cgril:public allcomers
{
public:int m_bh;cgril(){m_bh = 8;}void show(){cout << m_bh << " " << m_name << " " << m_tel << " " << endl;}
};
int main()
{cgril g1;g1.show();return 0;
}
(4)继承的适用场景:当新创建的类和原来的类有相同的成员变量和成员函数的时候,我们就可以使用继承,或者是类里面有很多的相似的地方的时候,我们就可以把这个共性的东西给提取出来,作为一个子类,这样就可以简化我们的代码;
例如这个各种各样的排序算法,我们都需要输入数据,输出数据,只不过就是这个排序算法的原理不相同罢了,这个时候,我们就可以把这个对于数据处理的部分封装到一个子类里面去;