C++程序设计分析 (基于谭书第三版)
因为某些考试中需要,而我也正好学习过其它编程语言,对C++也有些基础,所以往下划出了一些 谭书(第三版)的重点内容,谭书有一些不能过编译的代码和错误也修改了。下面将从 结构体开始,带你一步步踩坑谭书。
用户自定义数据类型
结构体类型
在c语言中,结构体的成员只能是数据,c++中既可以包括数据也可以包括函数。c中结构体 在使用的时候,比如Student是个结构体,那么使用必须 struct Student student1; 但是c++中可以直接写 Student studen1; 关于结构体 中分配内存单元
我们可以来试一试
#include<iostream>
using namespace std;
struct Student{
int num;
char name[20];
char sex;
int age;
float score;
char addr[30];
};
int main(){
char str[10] = {0};
cout<<sizeof(str)<<endl; //10
char strS[] ={"abc"};
cout<<sizeof(strS)<<endl; //此处输出4
cout<<4+20+1+4+4+30<<endl; //63
Student student1;
cout<<sizeof(Student); //输出68
return 0;
}
这里牵扯到内存对齐问题,但是非常坑的问题是,谭书上是这么说的
之后就是函数问题
#include<iostream>
using namespace std;
struct Student{
int num;
char name[20];
char sex;
int age;
float score;
char addr[30];
};
struct Test{
int num;
char h;
int fun(int a){
cout<<"结构体中输出函数为"<<a<<endl;
return a;
}
};
int main(){
cout<<sizeof(long)<<endl; //发现是4 可以证明这是32位系统的
char str[10] = {0};
cout<<sizeof(str)<<endl; //10
char strS[] ={"abc"};
cout<<sizeof(strS)<<endl; //此处输出4
cout<<4+20+1+4+4+30<<endl; //63
Student student1;
cout<<sizeof(Student)<<endl; //输出68
struct Test test;
test.fun(2); //调用函数
cout<<"Test占用字节是"<<sizeof(Test); //输出8
return 0;
}
这里明显可以看到为 8,而且这里long为4证明是32位系统,是68,而且我去64位系统上面测试,还是68。嗨呀。
指向结构体变量的指针
#include<iostream>
using namespace std;
#include<string.h>
int main(){
struct Test{
int num;
char name[10]={'h'};
int age;
};
struct Test test;
Test *p =&test;
p->num = 10;
strcpy((*p).name,"aaa");
cout<<sizeof(p->name)<<endl;
test.age = 20;
cout<<p->num<<" "<<(*p).name<< " "<<test.age;
return 0;
}
这个指针的3种方式
使用结构体完成简单的静态链表
下面复现谭书中的静态链表
//#define NULL 0;
#include <iostream>
using namespace std;
struct student
{
int num;
float score;
struct student *next;
};
int main(){
student a,b,c,*head,*p;
a.num = 31001;
a.score = 89.5;
b.num = 31003;
b.score = 90;
c.num = 31007;
c.score = 85;
head = &a;
a.next = &b;
b.next = &c;
c.next = NULL;
p = head;
do{
cout<<p->num << " "<<p->score<<endl;
p= p->next;
}while (p!=NULL);
return 0;
}
谭书中 NULL 0 这个建议别写,写了有红线看着烦。
枚举类型
谭枚举讲的有点尬,下面是对枚举的理解,以及对谭书的代码编写。
枚举类型的定义:
enum <类型名> {<枚举常量表>};
定义方式如:
enum color_set1 {RED, BLUE, WHITE, BLACK}; // 定义枚举类型color_set1
enum week {Sun, Mon, Tue, Wed, Thu, Fri, Sat}; // 定义枚举类型week
类型名称就是你自己随便定义的枚举类型的名称,枚举常量表 是以标识符形式表示的整型量,然后用逗号隔开,注意,这些枚举常量必须不相同。这里只是说标识符不相同,但是所直接赋得值能相同,比如下面这样
enum week{
Sun, Mon, Tue=8, Wed=8, Thu, Fri, Sat
};
(枚举常量只能以标识符形式表示,而不是整型、字符型等文字常量),比如下面这样:
enum letter_set {'a','d','F','s','T'}; //枚举常量不能是字符常量
enum year_set{2000,2001,2002,2003,2004,2005}; //枚举常量不能是整型常量
这里枚举常量的意思就是 枚举变量可能取得值,比如 week one。 one是个枚举变量,那这个one只能等于week这些枚举常量表中得东西,就像下面这样。
#include <iostream>
using namespace std;
enum week{
Sun, Mon, Tue, Wed, Thu, Fri, Sat
};
int main(){
week one;
//one = 2;//错误 不能将int类型的值分配到week类型的实体
one = Sun; //正确的赋值
cout<<one<<endl; //输出 0
}
有些人好奇这里面的值的,是的,在你自己没有指定值的情况下编译系统为每个枚举常量指定整数值,这个整数就是所列举元素的序号,序号从0开始。我们也可以为这些枚举指定值,但是这个指定要在声明的时候进行设置,当一个枚举常量设置值后,后面的枚举常量会依次加1,当然,这个设置值的类型就是整数值了。
#include <iostream>
using namespace std;
enum week{
Sun, Mon, Tue=8, Wed, Thu, Fri, Sat
};
int main(){
week one;
//one = 2;//错误 不能将int类型的值分配到week类型的实体
one = Sun; //正确的赋值
cout<<one<<endl; //输出 0
cout<<Tue<<endl; //8
cout<<Wed<<endl; //9
}
下面是枚举允许的一些操作
enum color_set1 {RED, BLUE, WHITE, BLACK} color1, color2;
enum color_set2 { GREEN, RED, YELLOW, WHITE} color3, color4;
color3=RED; //将枚举常量值赋给枚举变量
color4=color3; //相同类型的枚举变量赋值,color4的值为RED
int i=color3; //将枚举变量赋给整型变量,i的值为1
int j=GREEN; //将枚举常量赋给整型变量,j的值为0
//比较同类型枚举变量color3,color4是否相等
if (color3==color4) cout<<"相等";
//输出的是变量color3与WHITE的比较结果,结果为1
cout<< color3<WHITE;
允许的关系运算
//比较同类型枚举变量color3,color4是否相等
if (color3==color4) cout<<"相等";
//输出的是变量color3与WHITE的比较结果,结果为1
cout<< color3<WHITE;
重要提示
- 枚举变量可以直接输出,但不能直接输入。如:cout >> color3; //非法
- 不能直接将常量赋给枚举变量。如: color1=1; //非法
- 不同类型的枚举变量之间不能相互赋值。如: color1=color3; //非法
- 枚举变量的输入输出一般都采用switch语句将其转换为字符或字符串;枚举类型数据的其他处理也往往应用switch语句,以保证程序的合法性和可读性。
typedef
谭书上面这个说法非常好,都知道这个关键字是用来给类型取名字的,然后方便自己使用,但是定义的方式谭书上有个总结。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4TeWP74x-1631168409553)(C:\Users\zzh\AppData\Roaming\Typora\typora-user-images\image-20210831152059563.png)]
先按照定义变量的方法写出,替换变量的名字为你想要的取得名字,然后最前面加上typedef就好了。
然后谭书上对 3个较为复杂得方式进行了说明。
-
为数组 声明一个新的类型名
typedef int NUM[100];
-
为指针声明一个新的类型名
typedef char * STRING ;
STRING p,s[10];
-
为指向函数得指针 声明一个新的类型名(我觉得最好得还是在这里)
typedef int(* POINTER)();
POINT p1,POINT p2;
这里感觉指向函数得指针新的类型名称不是很好理解,so,下面看代码。
#include <iostream> using namespace std; typedef int (* FUN_P)(int,int); int fun(int a,int b){ return a+b; } int fun2(int a,int b){ return a*b; } FUN_P fun3(int a,int b){ if(a>b){ return fun; }else{ return fun2; } } int main(){ //指向函数的指针 FUN_P funp = fun; cout<<funp(1,2)<<endl; //输出3 //下面是函数返回值为指向函数的指针 cout<<fun3(3,3)(1,2)<<endl; //输出2 return 0; }
面向对象
这部分是建立在对面向对象的基础上的,java程序员应该非常好理解,这里不在阐述,因为概念的原因,这里可以在类内定义成员函数,也可以在类外定义成员函数,在定义类外成员函数的时候,比如有个Student类,我在类外定义成员函数,void Student:: display(){}
(注意,这个类外定义,必须在类内已经声明,否则报错)这个 ::
叫做作用域限定符或者称为作用域运算符,非常扯的是,这个名词经常在某些考试中出现,所以得记住。
比如下面代码:
#include <iostream>
using namespace std;
class Student{
private:
void display();
};
void Student::display(){
cout<<"我是类外定义的成员函数"<<endl;
}
int main(){
return 0;
}
你看,不受公私有的限制。
关于内置函数和在类中定义的成员函数
这里谭书也讲的不错。
在编译时将所调用函数的代码直接嵌入到主调函数当中,而不是将流程转出去。可以看到这样的写法:
这里就不在进行测试,有个很关键的问题是 在类中定义的不包括循环等控制语句的成员函数 c++默认给加了 inline,这就很扯了。
关于某些术语
一般在c++中类的声明和定义是分开的,声明写在头文件中,定义写在cpp,然后cpp被编译成目标文件,然后就可以对外不让人看到源码的使用了。
面向对象程序设计中有三个比较无语的名词:
对象、方法、消息
stud.display();
stud是对象,display()是方法,整个stud.display();
就是发给对象的消息,要求对象执行一个操作。
关于默认参数
如果在函数作原型声明时指定了默认参数,在定义函数的时候不必要重复去指定默认参数,如果在定义时也指定默认参数,其值应与函数声明时一致,如果不一致,编译系统以函数声明时指定的默认参数值为准,在定义函数时指定的默认参数值不起作用。
对象的初始化
谭书中是不让在类中直接进行赋值来初始化的,这是因为标准问题,在最新的c11标准中已经支持,但是有些考试指定此教材,所以我们以后初始化不要这样写(反正直接赋值本身就不符合规范,哪怕是在java中,也要改成在构造函数中赋值)
构造函数
下面基于谭书,对带参数的构造函数进行类外的定义
#include <iostream>
using namespace std;
class Box{
public:
Box(int,int,int);
int volume();
private:
int height;
int width;
int length;
};
Box::Box(int h,int w,int l){
height = h;
width = w;
length = l;
}
int Box::volume(){
return (height*width*length);
}
int main(){
Box box1(12,25,30);
cout<<"The volume of box1 is "<<box1.volume()<<endl;
Box box2(15,30,21);
cout<<"The volume of box2 is "<<box2.volume()<<endl;
return 0;
}
参数初始化表以及构造函数重载
之后就是用参数初始化表进行构造函数初始化,这个分为两种,一个类内和一个类外,然后顺带写了一个构造函数的重载。
初始化参数表主要的方式如下
Box(int h,int w,int l):height(h),width(w),length(l){}
如果是直接当成类的成员函数,那么需要写函数体(只要带上参数初始化表)。当然,也可以在类外定义函数的时候加上初始化参数表,在类内进行声明的时候只需要 Box(int h,int w,int l);就行了。
至于重载,参数个数、类型不同,函数名称相同,就是函数重载了。
看下面这个例子能看出东西。
#include <iostream>
using namespace std;
class Box{
public:
//类外定义的构造函数
Box(int,int,int);
//使用参数初始化表对数据成员进行初始化
Box(int h,int w,int l,int soso):height(h),width(w),length(l){
cout<<soso<<"什么神奇"<<endl;
};
//下面这个是错误的 必须有函数体
//Box(int h,int w,int l,int soso,double g):height(h),width(w),length(l);
//使用参数初始化表在类外进行定义
Box(int ,int,double);
int volume();
private:
int height;
int width;
int length;
};
Box::Box(int h,int w,int l){
height = h;
width = w;
length = l;
}
Box::Box(int h,int w,double l):height(h),width(w){
cout<<"构造函数类外定义 h"<<h<<"w "<< w<<" double l "<<l<<endl;
length = (int)l;
}
int Box::volume(){
return (height*width*length);
}
int main(){
Box box1(12,25,30);
cout<<"The volume of box1 is "<<box1.volume()<<endl;
Box box2(15,30,21);
cout<<"The volume of box2 is "<<box2.volume()<<endl;
Box box3(20,30,21,9999999);
cout<<"The volume of box3 is "<<box3.volume()<<endl;
Box box4(99,30,21.9);
cout<<"The volume of box4 is "<<box4.volume()<<endl;
return 0;
}
输出结果如下:
The volume of box1 is 9000
The volume of box2 is 9450
9999999什么神奇
The volume of box3 is 12600
构造函数类外定义 h99w 30 double l 21.9
The volume of box4 is 62370
使用默认参数的构造函数
函数中是允许写默认参数的,谭书中指出 当一个构造函数带默认参数的时候,实际上已经代表这个函数的空参数构造被实现了。
这里注意两个问题
-
如果函数的声明和函数的定义都写了默认参数,编译器是以声明中的为主的。不过谭书是建议在声明中写默认参数的,因为这样可以让读者看到传递的参数是什么样子的。
-
一旦使用带默认参数的构造函数,那么空参构造函数就不能写了,否则在使用的时候,程序不知道该使用哪一个。同样的,如果写和这个构造函数类似的重载(比如少一个参数),那么程序一样不知道该调用谁。谭书上如下:
下面复现谭书中的代码说明问题:
#include <iostream>
using namespace std;
class Box{
public:
Box(int h = 10,int w =10,int len =10);
int volume();
private:
int height;
int width;
int length;
};
Box::Box(int h,int w,int len){
height = h;
width = w;
length = len;
}
int Box::volume(){
return (height*width*length);
}
int main(){
Box box1; //没有给实参
cout<<"The volume of boxl is "<<box1.volume()<<endl;
Box box2(15); //只给定一个实参
cout<<"The volume of box2 is "<<box2.volume()<<endl;
Box box3(15,30);
cout<<"The volume of box3 is "<<box3.volume()<<endl;
Box box4(15,30,20);
cout<<"The volume of box4 is "<<box4.volume()<<endl;
return 0;
}
析构函数
注意下面几点:
析构函数无法被重载。
执行析构函数的时机:
- 如果在一个函数中定义了一个对象(假设是自动局部对象),当这个函数被调用结束时,对象应该被释放,在对象释放前自动执行析构函数。
- 静态局部对象在函数调用结束时对象并不释放,因此也不调用析构函数,只在main函数结束或调用exit函数结束程序时,才调用static局部对象的析构函数。
- 如果定义了一个全局对象,则在程序的流程离开其作用域时(如main函数结束或者调用exit函数)时,调用该全局对象的析构函数。
- 如果用new运算符动态地建立了一个对象,当用delete运算符释放该对象时,先调用该对象的析构函数。
调用构造函数和析构函数的顺序:
栈的形式,先进后出
调用构造和析构的时机:
- 如果在全局范围内定义对象(即在所有函数之外定义的对象),那么它的构造函数在本文件模块中的所有函数(包括main函数)执行前调用。但如果一个程序包含多个文件,而在不同的文件中都定义了全局对象,则这些对象的构造函数的执行顺序是不确定的。当main函数执行完毕,或调用exit函数时(此时程序终止),调用析构函数。
- 如果定义的是局部自动对象(例如在函数中定义对象),则在建立对象时调用其构造函数。如果对象所在的函数被多次调用,则在每次建立对象时都要调用构造函数。在函数调用结束、对象释放时先调用析构函数。
- 如果在函数中定义静态局部对象,则只在程序第1次调用此函数定义对象时调用构造函数一次,在调用函数结束时对象并不释放,因此也不调用析构函数,只在main函数结束,或调用exit函数结束程序时,才调用析构函数。
对象数组
对象数组的建立如下:
Student stud[50];
需要注意的是如果有50个元素,则会调用50次构造函数。
在定义时,可以提供实参来进行初始化。
Student stud[50] ={50,70,78};//这里要求构造函数只有一个参数。(当然如果有 设置了默认值的且多参的构造函数,这里会相当于只传第一个值) 但是如果多参的构造函数没有默认值,那么就只能用下面的方式来进行初始化
可以看一个示例
#include <iostream>
using namespace std;
class Student
{
private:
int age,num;
public:
Student(char *name,int age=2);
Student(int a,int b);
~Student();
};
Student::Student(char *name,int age)
{
cout<<"age是"<<age<<" name是"<<name<<endl;
}
Student::Student(int a,int b):age(a),num(b){
cout<<"a是"<<a<<" b是"<<b<<endl;
cout<<"age是"<<age<<" num是"<<num<<endl;
}
Student::~Student()
{
cout<<"析构函数调用"<<endl;
}
int main(){
char str[30] = "什么神奇";
Student(str,2);
Student stud[3] = {
Student(1001,200),
Student(1002,300),
Student(1003,400)
};
}
指向对象数据成员的指针和指向对象成员函数的指针
指向对象数据成员的指针比较简单:
int *p1;
p1 = &t1.hour;(因为.的优先级比&大,所以,会先运行.)
指向对象成员函数的指针
在定义指向普通函数指针的时候,我们可能会像这样
void (*p)();
p = fun;
(*p)();
但是在指向对象成员函数指针的时候,就不能这样定义了,必须要进行严格的区分。
void (Time::*p2)();//注意(Time::*p2)的括号不能省略
另外需要注意的一点是:
成员函数入口地址的写法是
&类名::成员函数名
不应该写成p3 = &t1.get_time;
下面可以看下谭书上写的代码:
#include <iostream>
using namespace std;
class Time{
public :
Time(int,int,int);
int hour;
int minute;
int sec;
void get_time();
};
Time::Time(int h,int m,int s){
hour = h;
minute = m;
sec = s;
}
void Time::get_time(){
cout<<hour<<":"<<minute<<":"<<sec<<endl;
}
int main(){
Time t1(10,13,56);
int * p1 = & t1.hour;
cout<<*p1<<endl;
t1.get_time();
Time * p2 = &t1;
p2->get_time();
void(Time::* p3)();
p3 = & (Time::get_time);
//不能像下面这样赋值
//p3 = &(t1.get_time);
//p3 = & (p2->get_time);
(t1.*p3)(); //调用对象t1中p3所指的成员函数(即t1.get_Time())
return 0;
}
this指针
谭书上的概念:
this它是指向本类对象的指针,它的值是当前被调用的成员函数所在的对象的起始地址。
理解的比较好的是下面谭书中的描述
-
关于 *this的使用
*this就是表示被调用的成员函数所在的对象。
共用数据的保护
const关键字在类中的使用
谭书中对const讲解的似乎有些反逻辑。
自己总结了下,并列出以下几点:
-
const在类对象中的使用
下面是个简单的const定义出来的常对象。
#include <iostream> using namespace std; class Time{ public : Time(int,int,int); int hour; int minute; int sec; void get_time(); }; Time::Time(int h,int m,int s){ hour = h; minute = m; sec = s; } void Time::get_time(){ cout<<hour<<":"<<minute<<":"<<sec<<endl; } int main(){ const Time time(1,2,3); //和下面的方式等价 Time const time2(12,4,5); return 0; }
下面我们可以看看这个常对象可以调用什么。
#include <iostream> using namespace std; class Time{ public : Time(int,int,int); int hour; int minute; int sec; void get_time(); }; Time::Time(int h,int m,int s){ hour = h; minute = m; sec = s; } void Time::get_time(){ cout<<hour<<":"<<minute<<":"<<sec<<endl; } int main(){ const Time time(1,2,3); //和下面的方式等价 Time const time2(12,4,5); cout<<time.hour<<" "<<time.minute<<" "<<time.sec; //cout<<time.get_time(); //常对象调用普通成员函数报错 //修改值 //time.hour = 2; //这里报错 表达式必须是可修改的左值 return 0; }
发现常对象可以使用普通数据成员,但是不能使用普通成员函数,并且不能对普通数据成员进行修改,那么记忆的方式可以认为是 常对象是部分只读的(普通成员函数没办法读取,因为不知道这个普通成员函数中是否修改了普通数据成员,只能一刀切了)。
那么,我想要读取这个普通成员函数怎么办,如果这样说法,是做不到的,特别是在所谓的普通成员函数中修改了数据成员的。但是如果,没有修改数据成员的,我们直接在这个成员函数后面加上const就ok了。就像下面的代码:
#include <iostream> using namespace std; class Time{ public : Time(int,int,int); int hour; int minute; int sec; void get_time(); void get_const_time() const; }; Time::Time(int h,int m,int s){ hour = h; minute = m; sec = s; } void Time::get_time(){ cout<<hour<<":"<<minute<<":"<<sec<<endl; } void Time::get_const_time() const{ //尝试修改数据成员但是失败了 //hour = 2; //报错,表达式必须是可修改的左值 cout<<hour<<":"<<minute<<":"<<sec<<endl; //get_time(); //尝试调用另一个普通成员函数,但是失败了。 } int main(){ const Time time(1,2,3); //和下面的方式等价 Time const time2(12,4,5); cout<<time.hour<<" "<<time.minute<<" "<<time.sec<<endl; //cout<<time.get_time(); //常对象调用普通成员函数报错 //修改值 //time.hour = 2; //这里报错 表达式必须是可修改的左值 time.get_const_time(); return 0; }
在末尾加了const的函数称为常成员函数。这个函数一样,既然能被常对象调用,而常对象本身不能修改数据成员,那么常成员函数能被常对象调用,当然也不能修改数据成员,同样,也不能调用普通成员函数。也是只读。
这里记住一点,我一旦 const对象了,我就是要保证我不能修改数据成员,那么谁会容易修改数据成员,当然是成员函数,那么我不知道这个成员函数是否修改了数据成员,那么就直接不让用,如果非要使用,你就必须给我加上不能修改数据成员的限制,然后就是引出常成员函数。
当然,为了方便,常成员函数非要修改某些数据成员,我们也可以做,在这个数据成员上加上 mutable关键字,声明为可变的数据成员。
#include <iostream> using namespace std; class Time{ public : Time(int,int,int); int hour; int minute; int sec; mutable int wa; void get_time(); void get_const_time() const; }; Time::Time(int h,int m,int s){ hour = h; minute = m; sec = s; wa = 3; } void Time::get_time(){ cout<<hour<<":"<<minute<<":"<<sec<<endl; } void Time::get_const_time() const{ //尝试修改数据成员但是失败了 //hour = 2; //报错,表达式必须是可修改的左值 cout<<hour<<":"<<minute<<":"<<sec<<endl; //get_time(); //尝试调用另一个普通成员函数,但是失败了。 //修改这个可变的数据成员成功 wa = 2; cout<<wa<<endl; } int main(){ const Time time(1,2,3); //和下面的方式等价 Time const time2(12,4,5); cout<<time.hour<<" "<<time.minute<<" "<<time.sec<<endl; //cout<<time.get_time(); //常对象调用普通成员函数报错 //修改值 //time.hour = 2; //这里报错 表达式必须是可修改的左值 time.get_const_time(); return 0; }
-
const定义的数据成员(常数据成员)
初始化
直接用const修饰的数据成员,叫做常数据成员,既然是常,那么就是不可变的,那么必须要有个初始化的值(不然报错)。 既然是数据成员,那么会有一个初始化的问题,这个初始化应该在哪里呢,在c11之前,不能在类中直接赋值,那么我们要在构造函数中赋值,但是常数据成员能在构造函数中赋值吗?当然是不行的,记住,常数据成员只有一种初始化方式,就是使用参数初始化表。
能被谁调用
既然初始化结束了,那么谁能使用呢? 想要使用这个常数据成员,目前无非就四个,一个是普通成员函数,一个是常成员函数,还有一个是常对象直接.去访问,和普通对象直接点去访问。
#include <iostream> using namespace std; class Time{ public : Time(int,int,int); int hour; int minute; int sec; mutable int wa; void get_time(); void get_const_time() const; const int num; }; //这个构造函数中必须包含 这个常数据成员 num的初始化,不然编译不过,而这个初始化必须使用参数初始化表 Time::Time(int h,int m,int s):num(10){ hour = h; minute = m; sec = s; wa = 3; } void Time::get_time(){ //普通成员函数访问常数据成员 cout<<num<<endl; cout<<hour<<":"<<minute<<":"<<sec<<endl; } void Time::get_const_time() const{ //尝试修改数据成员但是失败了 //hour = 2; //报错,表达式必须是可修改的左值 cout<<hour<<":"<<minute<<":"<<sec<<endl; //get_time(); //尝试调用另一个普通成员函数,但是失败了。 //修改这个可变的数据成员成功 wa = 2; cout<<wa<<endl; //常成员函数访问常数据成员 cout<<num<<endl; } int main(){ const Time time(1,2,3); //和下面的方式等价 Time const time2(12,4,5); Time time3(12,3,5); cout<<time.hour<<" "<<time.minute<<" "<<time.sec<<endl; //cout<<time.get_time(); //常对象调用普通成员函数报错 //修改值 //time.hour = 2; //这里报错 表达式必须是可修改的左值 time.get_const_time(); //常对象访问常数据成员 cout<<time.num<<endl; //普通对象访问常数据成员 cout<<time3.num<<endl; //普通对象访问常成员函数 time3.get_const_time(); time3.get_time(); return 0; }
这里发现都是能够使用的,想想也知道,这就是个相当于常量的东西,我要做的只是让它不改变。这里检测一个常量是否要改变那非常简单,所以没有其它限制。
-
const定义的成员函数(常成员函数)
上面基本上体会的差不多了。明确一个点,const就是为了让东西不改变,常对象是为了让数据成员不改变,常成员函数也是限制数据成员不改变,所以常对象只能调用常成员函数,不能调用普通函数,因为不知道是否改变了数据成员,那么基于这个限制,所有的限制就是为了防止数据成员不变,这就简单了。
普通函数能调用常成员函数吗,当然能,因为这里面数据成员不变呀。普通对象能调用吗,当然也能呀,还是因为调用的时候我就是使用常成员函数呀,而常成员函数已经保证数据成员不变。
那么常成员函数中能调用普通成员函数吗,当然不行呀,因为数据成员改不改由普通成员函数决定,到底改没改,编译器不知道呀。
总结,一句话,谁能调谁,谁不能调谁,那么加了const就是为了保证数据成员不改变,哪些可能会改,那就不能调用。
指向对象的常指针和指向常对象的指针变量
Time * const ptrl = &tl;
写在变量名前面,表示这个指针变量本身不能改变指向了,就是指向对象的常指针。(名字不要紧)
const char * ptr;
指向常对象的指针变量,写在最前面,表示无法修改指针指向的内容了。
然后就是这些能指向的问题,如果一个对象已经被声明为常对象,只能用指向常对象的指针变量指向它,而不能用一般的(指向非const型对象的)指针变量去指向它。
如果定义了一个指向常对象的指针变量,并且使他指向一个非const的对象,则其指向的对象是不能通过指针变量来改变的。
这一块根据定义本身就能理解了,我指针指向的内容不能被改变,那么我当然适用于常对象,那我常对象应该不能被改变值,那我当然只有指向常对象的指针变量才能指向了,这个内容不能修改无非就是最前面加个const而已。
常引用
void fun(const Time &t);
这里实际上就是不能改变引用t所指向变量的值。和上面的常量指针大同小异。(如果你不知道引用是什么 ,引用就是变量的别名而已,没什么好说的)
对于const相关,谭书给了下面的总结,但是我总觉得这是完美符合逻辑的事情,不需要特殊记忆。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JIw1xOo8-1631168409558)(C:\Users\zzh\AppData\Roaming\Typora\typora-user-images\image-20210902141615231.png)]
复制构造函数
谭书上的形式如下:
Box::Box(const Box&b){//const可加可不加,最好加上
}
复制构造函数调用的时机:
-
程序中需要新建立一个对象,并用另一个同类的对象对它初始化。如下:
Box box2 = box1
-
当函数的参数作为类的对象时。
-
函数的返回值是类的对象。
第一点都能想明白,第二点因为要实现实参传给形参,除了引用传递,其它都是要拷贝的呀(引用就是表面上不拷贝,实际上会有个变量只不过里面存放了地址,被编译器给隐藏了,只不过就算要拷贝也是拷贝地址,和内容毫无关联,先无需理会),
这里有个坑呀,各位,就是当函数的返回值是类的对象时候,g++编译器给我把它优化了,因此这里没有调用复制构造函数。用g++的同学们使用这个命令,在编译命令中加上“-fno-elide-constructors”参数,例g++ -fno-elide-constructors testReturn.cpp 。
就可以看到复制构造函数的调用了。
看下面的代码
#include <iostream>
using namespace std;
class Time{
public :
Time(int,int,int);
Time(const Time & time);
int hour;
int minute;
int sec;
void get_time();
};
//这个构造函数中必须包含 这个常数据成员 num的初始化,不然编译不过,而这个初始化必须使用参数初始化表
Time::Time(int h,int m,int s){
hour = h;
minute = m;
sec = s;
cout<<"构造函数调用"<<endl;
}
//复制构造函数 其实编译器默认就是这些代码
Time::Time(const Time &time){
this->hour = time.hour;
this->minute = time.minute;
this->sec = time.sec;
cout<<"复制构造函数调用"<<endl;
}
Time test(){
Time time(12,3,4);
return time;
}
int main(){
Time time(999,34,5);
//需要注意的是这里被g++编译器优化了,所以在vscode下不被执行这个复制构造函数
//但是需要注意,谭书说能,那它就一定能(不然你考试过不了,你信不),比如你回到vc6.0
time = test();
cout<<time.hour<<endl;
return 0;
}
PS C:\c> g++ -fno-elide-constructors .\对象的复制.cpp
PS C:\c> .\a.exe
构造函数调用
构造函数调用
复制构造函数调用
12
静态成员
静态数据成员和静态成员函数
这个地方和java的差不多。
静态数据成员和静态成员函数不在一个对象的内存当中,不信你sizeof看看。
静态数据成员两个要点
-
初始化 必须要在类外进行初始化,如果不初始化,默认是0
比如
int Box::height =10;
-
能被谁用 谁都能用 ,一个中通过类名直接调用,一个是通过对象直接调用。
比如 Box::heigth;
静态成员函数有两个要点:
-
静态成员函数只能访问静态的数据成员,当然你可以将一个对象作为参数传递进来使用
-
静态成员函数可以被类名或者对象直接调用使用
比如: Student::average()
友元
友元作为c++破坏封装的一大特色实属有必要进行了解,来看看这个货是怎么破坏封装然后在谭书上的体现把。
友元的基本思想就是为了让一个或多个函数可以访问一个类中的私有数据成员,以及来破坏面向对象的基本思想。这样一想,就会引出下面这几种操作,普通函数做友元,成员函数做友元,类做友元。
将普通函数声明为友元函数
friend void display(Time &);
形式呢就是在函数的最前面加个friend,没什么东西。下面直接上代码。
#include <iostream>
using namespace std;
class Time{
public :
Time(int,int,int);
friend void display(Time &); //这里 不受 public private等限制 随便放置位置
private:
int hour;
int minute;
int sec;
};
Time::Time(int h,int m,int s){
hour = h ;
minute = m;
sec = s;
}
void display(Time &t){
cout<<t.hour<<":"<<t.minute<<":" <<t.sec<<endl; //这样就可以访问一个类的私有成员了
}
int main(){
Time t1(10,13,56);
display(t1);
return 0;
}
将成员函数声明为友元函数
这个无非就是为了让成员函数能够访问另一个类的私有成员,所以要将其声明为这个类的友元函数,为了防止编译报错,需要使用类的提前引用声明。
#include <iostream>
using namespace std;
class Date;
class Time{
public :
Time(int,int,int);
void display(Date &);
private:
int hour;
int minute;
int sec;
};
class Date{
public:
Date(int,int,int);
friend void Time::display(Date &);
private:
int month;
int day;
int year;
void fun(){
cout<<"我是一个测试"<<endl;
}
};
Time::Time(int h,int m,int s){
hour = h ;
minute = m;
sec = s;
}
void Time::display(Date &d){
cout<<d.month<<"/"<<d.day<<"/"<<d.year<<endl; //引用date类对象中的私有数据
d.fun();
cout<<hour<<":"<<minute<<":"<<sec<<endl; //引用本类对象中的私有数据
}
Date::Date(int m,int d,int y){
month = m;
day = d;
year = y;
}
int main(){
Time t1(10,13,56);
Date d1(12,15,2004);
t1.display(d1);
return 0;
}
友元类
A作为B的友元类那么A就能访问 B中的私有成员了。就是那么简单。。。
谭书中不愿写代码,我来写一个
class Building;
class goodGay
{
public:
goodGay();
void visit();
private:
Building *building;
};
class Building
{
//告诉编译器 goodGay类是Building类的好朋友,可以访问到Building类中私有内容
friend class goodGay;
public:
Building();
public:
string m_SittingRoom; //客厅
private:
string m_BedRoom;//卧室
};
Building::Building()
{
this->m_SittingRoom = "客厅";
this->m_BedRoom = "卧室";
}
goodGay::goodGay()
{
building = new Building;
}
void goodGay::visit()
{
cout << "好基友正在访问" << building->m_SittingRoom << endl;
cout << "好基友正在访问" << building->m_BedRoom << endl;
}
void test01()
{
goodGay gg;
gg.visit();
}
int main(){
test01();
system("pause");
return 0;
}
运算符重载
emmm,这里比较坑,谭书上对这个的篇幅展开较多,也非常详细,这里不会都叙述。
首先对运算符重载有个认识,首先什么是运算符,其实就是c++中使用的各种符号,就是运算符。 比如 +
。那么什么叫运算符重载呢,比如 我在 1+1系统能算出来 但是 如果我 Student stu1,stu2; 然后 stu1+stu2呢,编译器能算出来吗,肯定不行。然后c++为了能让它算出来,就提供了运算符重载。
重载有两种 模式,一个是 作为成员函数进行重载,一个是作为友元函数进行重载。
思考一下 假设 我 stu1+stu2,我先要写一个正常的函数应该怎么写呢,当然是 Student add(Student & stu1,Student &stu2);
编译器要识别这种类似于加函数的东西,定制了他的一套规则,来看下谭书上写的代码。
#include <iostream>
using namespace std;
class Complex
{
private:
double real;
double image;
public:
Complex(){
real = 0;image = 0;
};
Complex(double r,double i){
real = r;image = i;
}
friend Complex operator +(Complex &cl,Complex &c2);
void display();
};
Complex operator +(Complex &c1,Complex &c2){
return Complex(c1.real+c2.real,c1.image+c2.image);
}
void Complex::display(){
cout<<"("<<real<<","<<image<<"i)"<<endl;
}
int main(){
Complex c1(3,4),c2(5,-10),c3;
c3 = c1+c2;
cout<<"c1=";
c1.display();
cout<<"c2=";
c2.display();
cout<<"c1+c2=";
c3.display();
}
上面是做为友元函数写的,再来看个作为成员函数的,因为成员函数包含自己本身对象,那在函数参数中肯定只需要一个参数就行了,来对应上 + 号的左右两边,+ 号左边是 当前对象,加号右边是 要加的对象。 emmm,为了兼容上面的代码,写个减号的。
#include <iostream>
using namespace std;
class Complex
{
private:
double real;
double image;
public:
Complex(){
real = 0;image = 0;
};
Complex(double r,double i){
real = r;image = i;
}
friend Complex operator +(Complex &cl,Complex &c2);
Complex operator -(Complex &);
void display();
};
Complex operator +(Complex &c1,Complex &c2){
return Complex(c1.real+c2.real,c1.image+c2.image);
}
Complex Complex::operator-(Complex &c1){
return Complex(this->real - c1.real,this->image-c1.image);
}
void Complex::display(){
cout<<"("<<real<<","<<image<<"i)"<<endl;
}
int main(){
Complex c1(3,4),c2(5,-10),c3,c4;
c3 = c1+c2;
c4 = c1 - c2;
cout<<"c1=";
c1.display();
cout<<"c2=";
c2.display();
cout<<"c1+c2=";
c3.display();
cout<<"c1-c2=";
c4.display();
}
关于前置和后置
运算符重载就那样,尤其是双目运算符的重载。当然还有些对单目运算符,比如 ++i i++这种。这里需要注意的地方就是 i++的后置 重载不同,具体的代码可以看下面。
前置递增是直接增加,但是后置递增是先返回当前,然后再进行自加。对于对象来说,我们要重载后置递增,可以先复制一个当前对象,然后对原有对象进行自增,然后返回复制的对象就行了。 就可以保证,如 int a =2 ;a++;一般效果的后置递增。
class MyInteger {
friend ostream& operator<<(ostream& out, MyInteger myint);
public:
MyInteger() {
m_Num = 0;
}
//前置++
MyInteger& operator++() {
//先++
m_Num++;
//再返回
return *this;
}
//后置++
MyInteger operator++(int) {
//先返回
MyInteger temp = *this; //记录当前本身的值,然后让本身的值加1,但是返回的是以前的值,达到先返回后++;
m_Num++;
return temp;
}
private:
int m_Num;
};
ostream& operator<<(ostream& out, MyInteger myint) {
out << myint.m_Num;
return out;
}
//前置++ 先++ 再返回
void test01() {
MyInteger myInt;
cout << ++myInt << endl;
cout << myInt << endl;
}
//后置++ 先返回 再++
void test02() {
MyInteger myInt;
cout << myInt++ << endl;
cout << myInt << endl;
}
int main() {
test01();
//test02();
system("pause");
return 0;
}
转换构造函数和类型转换函数
转换构造函数的作用是将一个其它类型的数据转换成一个类的对象。
转换构造函数的形式:
Complex(double r){real = r;imag = 0;}
这个区别于一般的构造函数,只能有一个形参。
类型转换函数
类型转换函数的作用是将一个类的对象转换成另一类型的数据。
operator double(){
return real;
}
这个函数就返回 double型变量real的值。
它的作用是将一个类对象转换成为一个double型数据。
来看下面的联合代码
#include <iostream>
using namespace std;
class Complex{
public:
Complex(){
real = 0;
imag = 0;
}
Complex(double r){
real = r;
imag = 0;
}
Complex(double r,double i){
real = r;
imag = i;
}
friend Complex operator+(Complex c1,Complex c2);
void display();
private:
double real;
double imag;
};
Complex operator+(Complex c1,Complex c2){
return Complex(c1.real+c2.real,c1.imag+c2.imag);
}
void Complex::display(){
cout<<"("<<real<<","<<imag<<"i)"<<endl;
}
int main(){
Complex c1(3,4),c2(5,-10),c3;
c3 = c1+2.5;
c3.display();
return 0;
}
你会发现,转换构造函数的用法。
c3是个 Complex类型,那是2.5是个double,在进行相加的时候,先看+号找不到对应的运算符重载的函数,然后去找转换构造函数,发现咦,有转换构造函数呀,那就使用了。然后将2.5转成 Complex类型,然后相加了。
当然,这里有个奇怪的问题了,如果我写个类型转换函数。
#include <iostream>
using namespace std;
class Complex{
public:
Complex(){
real = 0;
imag = 0;
}
//转换构造函数
Complex(double r){
real = r;
imag = 0;
}
Complex(double r,double i){
real = r;
imag = i;
}
//类型转换函数
operator double(){
return real;
}
friend Complex operator+(Complex c1,Complex c2);
void display();
private:
double real;
double imag;
};
Complex operator+(Complex c1,Complex c2){
return Complex(c1.real+c2.real,c1.imag+c2.imag);
}
void Complex::display(){
cout<<"("<<real<<","<<imag<<"i)"<<endl;
}
int main(){
Complex c1(3,4),c2(5,-10),c3;
//c3 = c1+2.5; //此时代码将会出现二义性
c3.display();
return 0;
}
这里将会出现二义性,到底是调用转换构造函数呢,还是使用类型转换函数呢。
一种理解是,调用转换构造函数,把2.5变成Complex类对象,然后调用运算符“+”重载函数,与c1进行复数相加。另外一种理解是,调用类型转换函数,把c1转换成double型数,然后与2.5进行相加。系统无法进行判定,这二者是矛盾的。如果要使用类型转换函数,就应当删除运算符“+”重载函数。
面向对象程序设计
c++里面这些简单的什么派生、继承就不说了。注意这里面有个什么 公有继承、保护继承、私有继承,意思呢,就是继承过来的东西,变成什么类型,比如私有继承,继承过来的就是变成了我当前类的私有属性。如果是公有继承,那么 基类中的公有和保护在子类中一样是公有和保护。如果是保护,那么公有也会变成保护。
有个图,可以非常好的解释
多级继承以及构造函数的设置方法
如果多级继承,那么肯定涉及到基类的构造函数调用,如果基类拥有无参构造,那么当然可以不在派生类中阐述,如果基类有,就需要表达出来了。如果你的派生类中有定义到基类的对象,那么,当然,你也需要在构造函数的参数初始化表中进行编写。来看下面这个代码:
谭书上这里的代码时有问题的,因此我对它进行了修改。
#include <iostream>
#include <string>
using namespace std;
class Student
{
public:
Student(int n,char nam[10]){
num = n;
name = nam;
}
void display(){
cout<<"num: "<<num<<endl;
cout<<"name: "<<name<<endl;
}
protected:
int num;
char* name;
};
class Student1:public Student{
public:
Student1(int n,char name[10],int a):Student(n,name){
age = a;
}
void show(){
display();
cout<<"age: "<<age<<endl;
}
private:
int age;
};
class Student2:public Student1{
public:
Student2(int n,char name[10],int a,int s):Student1(n,name,a),one(n,name){
score = s;
}
void show_all(){
show();
cout<<"score:"<<score<<endl;
}
private:
int score;
Student one;
};
int main(){
char name [10] = "li";
Student2 stud(10010,name,17,89);
stud.show_all();
return 0;
}
谭书上这里一会String一会char [],这里会出现非常大的问题。所以这里直接修改了。
多继承以及菱形继承
c++是允许多继承的,这里难免会出现构造函数初始化的问题,还有万一呢,变量名称相同或者是菱形继承的问题。
先看多继承。
下面以谭书上的例子,来写。 但是,谭书的代码是不能通过编译的,所以这里进行了一些修改。
#include <iostream>
#include <string.h>
using namespace std;
class Teacher{
public:
Teacher(char* nam,int a ,char* t){
name = nam;
age =a;
title = t;
}
void display(){
cout<<"name:"<<name<<endl;
cout<<"age"<<age<<endl;
cout<<"title:"<<title<<endl;
}
protected:
char* name;
int age;
char* title;
};
class Student{
public:
Student(char nam[],char s,float sco){
strcpy(name1,nam);
sex = s;
score = sco;
}
void display1(){
cout<<"name:"<<name1<<endl;
cout<<"sex"<<sex<<endl;
cout<<"score:"<<score<<endl;
}
protected:
char* name1;
char sex;
float score;
};
class Graduate:public Teacher,public Student{
public :
Graduate(char* nam,int a ,char s,char * t,float sco,float w):Teacher(nam,a,t),
Student(nam,s,sco),wage(w){}
void show(){
cout<<"name:"<<name<<endl;
cout<<"age:"<<age<<endl;
cout<<"sex:"<<sex<<endl;
cout<<"score:"<<score<<endl;
cout<<"title:"<<title<<endl;
cout<<"wages:"<<wage<<endl;
}
private:
float wage;
};
int main(){
char name[] = "Wang_li";
char title[] = "assistant";
Graduate gradl(name,24,'f',title,89.5,1200);
gradl.show();
return 0;
}
上面主要是多继承的构造函数编写方法,其实就是 依次依靠参数初始化表来进行初始化而已。
其它的,为什么Student会有一个叫做name1的,就是为了防止和Teacher中name重复,但是因为Graduate类需要继承这两个类,所以,如果名字一旦相等,直接编译报错。
那么,问题来了,有木有办法解决这个问题,根据谭书上说,解决这个问题,只有一种方案,需要指定其作用域。
class Graduate:public Teacher,public Student{
public :
Graduate(char* nam,int a ,char s,char * t,float sco,float w):Teacher(nam,a,t),
Student(nam,s,sco),wage(w){}
void show(){
//这里设定作用域
cout<<"name:"<<Teacher::name<<endl;
cout<<"age:"<<age<<endl;
cout<<"sex:"<<sex<<endl;
cout<<"score:"<<score<<endl;
cout<<"title:"<<title<<endl;
cout<<"wages:"<<wage<<endl;
}
private:
float wage;
};
然后又是菱形继承的问题,首先什么是菱形继承
然后盗用谭书上的一个图片:
当N类里面有某个成员被继承,那么A和B类都会有这个成员,那么C又继承了A和B,然后就出问题了,我这个N类中的成员,到底是什么鬼。就会导致,明明一个成员,但是确实有两份拷贝。
继续抄谭书上的某个代码,虽然又发现错的了,还得修改
#include <iostream>
using namespace std;
class N{
public:
int a;
void display(){
cout<<a<<endl;
}
};
class A:public N{
public:
int a1;
};
class B:public N{
public:
int a2;
};
class C:public A,public B{
public:
int a3;
void show(){
cout<<"a3="<<a3<<endl;
}
};
int main(){
C c1;
//c1.a;
return 0;
}
注意这里如果直接 c1.a 会直接导致
那么,明确一下好了,如果直接拿N去明确:
又会导致这个错误。
这里有问题的是,无法知道到底使用的是哪个派生类中的a,有可能这个数值已经发生改变,所以,需要直接使用N的直接派生类 A或者B来进行指出。
但是如果类简单还好,一旦类的关系复杂,会导致空间占用大,而且编码完全变成了一种奇怪的约定。比如这里,谁知道你是用B的数据还是A的数据呢,本来就是同一个数据。
所以,这里,出来了虚基类。
谭书上的描述:
C++提供虚基类的方法,使得在继承间接共同基类时只保留一份成员。
可以理解一种指针。
看下谭书中的方法
class 派生类名: virtual继承方式 基类名
#include <iostream>
using namespace std;
class N{
public:
int a;
void display(){
cout<<a<<endl;
}
};
class A : virtual public N{
public:
int a1;
};
class B:virtual public N{
public:
int a2;
};
class C:public A,public B{
public:
int a3;
void show(){
cout<<"a3="<<a3<<endl;
}
};
int main(){
C c1;
c1.a=3;
return 0;
}
此时的编译将不会报错。
当然,如果这里还牵扯到一个初始化的问题,可以看下面的例子。
#include <iostream>
using namespace std;
class N{
public:
int a;
N(int h){
cout<<"N构造函数初始化"<<h<<endl;
}
void display(){
cout<<a<<endl;
}
};
class A : virtual public N{
public:
A(int b):N(fun(b)){
a1 = b;
cout<<"A构造函数初始化"<<b<<endl;
}
int fun(int b){
return b+2;
}
int a1;
};
class B:virtual public N{
public:
B(int c):N(c){
cout<<"B构造函数初始化"<<c<<endl;
}
int a2;
};
class C:public A,public B{
public:
//此处必须要求最开始的基类的初始化 否则报错
C(int d):A(d),B(d),N(d){
cout<<"C的构造函数初始化"<<d<<endl;
}
int a3;
void show(){
cout<<"a3="<<a3<<endl;
}
};
int main(){
C c1(3);
return 0;
}
需要注意的就是在最终派生类c的构造函数中必须要包装基类N的初始化。
然后看下输出结果就能知道调用顺序
PS C:\c> .\a.exe
N构造函数初始化3
A构造函数初始化3
B构造函数初始化3
C的构造函数初始化3
PS C:\c>