概述
上篇学习了C语言,这篇继续学习C++,这篇是建立在C基础上的
真的是码字如蜗牛,写了好久才写这么多,作为笔记吧,防止以后忘记
先写一个Hello world
首先打开你的文本编辑器,输入一下内容
#include <iostream>
using namespace std;
int main()
{
cout << "Hello, world!!!!!" << endl;
return 0;
}
然后把文件保存为test.cpp
,最后编译执行
L-96FCG8WP-1504:untitled renxiaohui$ g++ test.cpp
L-96FCG8WP-1504:untitled renxiaohui$ a.out
Hello, world!!!!!
C++的程序结构
#include <iostream>
using namespace std;
// main() 是程序开始执行的地方
int main()
{
cout << "Hello World"; // 输出 Hello World
return 0;
}
- 第一行 添加头文件,和C的作用一样
- 第二行
using namespace std;
命名空间,是C++新加的概念 int main()
程序的入口cout << "Hello World"
在屏幕上输出Hello worldreturn 0;
终止main函数
什么是命名空间
为什么要写using namespace std
这句话呢?
这是C++引入的一个新的机制,主要是为了解决多个模块间命名出冲突的问题,就像现实人的名字重名一个道理,C++把名字相同的放到不同的空间中,来防止命名的冲突
例如标准的C++库提供的对象都放在std这个标准的命名空间中,比如cin,cout,endl,所以我们看到C++程序中,都会有using namespace std
这句话
比如这个程序
#include<iostream>
using namespace std;
int main()
{
cout<<"Nice to meet you!"<<endl;
return 0;
}
这里面用到了cout和endl则必须提前告知用std命名空间,除此之外还有俩种写法
第二种写法,用域限定符来逐个制定
#include<iostream>
int main()
{
std::cout<<"Nice to meet you!"<<std::endl;
return 0;
}
第三种,用using和域限定符一起制定用那些名字
#include<iostream>
using std::cout;
using std::endl;
int main()
{
cout<<"Nice to meet you!"<<endl;
return 0;
}
C++中的输入输出
C++中的输入输出除了兼容C的写法及使用printf
和scanf
实现外,C++还有自己的一套输入输出的写法,C++的输入流和输出流分别使用cin
和cout
来表示,使用之前需要引入标准库iostream,即 #include <iostream>
cout 输出流的使用
cout输出流需要搭配<< 输出操作符来使用,例如输出语句
cout << "hello world";
会在屏幕上显示hello world
本质是将hello world 插入cout 对象,并以cout对象为返回值返回,因此可以在后方接多个<<
cout << "hello world" << "kk";
cout
经常和endl
配合使用,起到换行符的作用
cout << "hello" << endl<<"world"<<endl;
输出
hello
world
cin 输入流的使用
接收一个数据前,都要先定义一个与之类型相一致的变量,然后利用cin 和>> 从用户的键盘接受输入
#include <iostream>
using namespace std;
int main() {
int a;
cout << "请输入你的数字" << endl;
cin >> a;
cout << a << endl;
return 0;
}
输出
请输入你的数字
12
12
cin也可以接收多个变量
int a,b;
cin>>a>>b;
基本数据类型
类型 | 关键字 |
---|---|
布尔型 | bool |
字符型 | char |
整形 | int |
浮点型 | float |
双浮点型 | double |
无类型 | void |
宽字符型 | wchar_t |
其实wchar_t是这样来的
typedef short int wchar_t;
所以一个wchar_t和short int 空间一样
一些基本数据类型可以用一个或多个修饰符修饰
- signed
- unsigned
- short
- long
下表显示了各种变量类型所占内存和能储存的最大值和最小值
类型 | 位 | 范围 |
---|---|---|
char | 1个字节 | -128到127或0到255 |
unsigned char | 1个字节 | 0到255 |
signed char | 1个字节 | -128到127 |
int | 四个字节 | -2147483648 到 2147483647 |
unsigned int | 四个字节 | 0 到 4294967295 |
signed int | 四个字节 | -2147483648 到 2147483647 |
short int | 俩个字节 | -32768 到 32767 |
unsigned short int | 俩个字节 | 0 到 65,535 |
signed short int | 俩个字节 | -32768 到 32767 |
long int | 8个字节 | -9,223,372,036,854,775,808 到 9,223,372,036,854,775,807 |
signed long int | 8个字节 | -9,223,372,036,854,775,808到9,223,372,036,854,775,807 |
unsigned long int | 8个字节 | 0 到 18,446,744,073,709,551,615 |
float | 四个字节 | 精度型占4个字节(32位)内存空间,+/- 3.4e +/- 38 (~7 个数字) |
double | 8个字节 | 双精度型占8 个字节(64位)内存空间,+/- 1.7e +/- 308 (~15 个数字) |
long double | 16个字节 | 长双精度型16个字节(128位)内存空间,可提供18-19位有效数字。 |
wchar_t | 2或4个字节 | 一个宽字符 |
带默认形参的函数
在C++中允许在自定义定的函数的形参列表中,给一个形参的默认值,这样如果调用的时候有参数,那么按传递的参数来说,如果没有则按照默认的参数
#include <iostream>
using namespace std;
int add(int a =4,int b =5){
return a+b;
}
int main(int argc, const char * argv[]) {
int a= add();
int b = add(7,8);
cout<<a<<"/"<<b<<endl;
return 0;
}
输出
9/15
函数模板
C++和java一样都有函数的重载,函数重载可以处理多种类型,同一个方法名,但依然需要分开定义,如果可以让他代码更精简一点,模板化就更好了
为此C++提供了函数模板这一机制,可以大大提高函数的通用性
函数模板可以创建一个通用的函数,可以支持多种形参
用关键字template
来定义,形式如下
template<class 类型名1,class 类型名2…>
返回值 函数名(形参表列) 模板参数表
{
函数体
}
上面template<class 类型名1,class 类型名2…>
是一个声明语句,template
是定义模板函数的关键字,尖括号里可以有多个类型,前面都要用class(或typename来定义),然后后面跟定义的函数模板,中间不可用加其他语句
#include <iostream>
using namespace std;
template<class T1,class T2>
T1 add(T1 x,T2 y){
cout<<sizeof(T1)<<"/"<<sizeof(T2)<<endl;
return x+y;
}
int main(int argc, const char * argv[]) {
cout<<add(1,2)<<endl;
cout<<add(0.22, 0.33)<<endl;
cout<<add('A',2)<<endl;
return 0;
}
输出
4/4
3
8/8
0.55
1/4
C
内联函数
一个函数被另一个函数调用的时候才会有生命,才会为其准备相应的空间,在调用完毕之后才会清理释放结束
可以看到每一次的函数调用都会带来一些空间上的花销
定义一个函数的作用,也是为了提高代码的重用性,可以在需要的时候调用,提高开发效率,那么一个代码本身就不多,又被频繁调用的函数,这样做是否合算呢?
C++已经考虑到了这个问题,为我们提供了内联机制,即仍然是使用自定义函数,但是编译的时候,把函数代码插入到函数调用的地方, 就像普通的程序执行代码一样,来解决这个问题
用法非常简单,只需要在函数定义前加上inline
关键字声明就可以了
#include <iostream>
using namespace std;
inline int max1(int x,int y){
return x>y?x:y;
}
int main(int argc, const char * argv[]) {
cout<<max1(12, 2)<<endl;
cout<<max1(2, 13)<<endl;
return 0;
}
内联函数的调用要出现在调用之前,才可以让编译器了解上下文进行代码替换
类的定义
类的含义就不多赘述了,直接看如何使用
第一种定义形式
class Status{
public:
int a;
char name[100];
int socre;
int print(){
cout<<a<<"/"<<name<<"/"<<socre<<endl;
return 0;
}
};
其实跟java类的定义差不多,其中public
是访问权限控制符,C++还有其他的权限控制符private,protected
private: 表示私有,他所修饰的成员,只能在类的内部访问,外界不能访问
protected: 除了类内部可以访问,他的子类也可以访问
public: 内部外部都可以访问
类的最后有一个分号,不要忘记了
第二种形式
class Sts{
public:
int a;
char name[100];
int socre;
int print();
};
int Sts::print(){
cout<<a<<"/"<<name<<"/"<<socre<<endl;
return 0;
}
这种形式成员函数,仅仅在类内声明函数原型,再类外定义函数体,在类外定义函数体的需要类名加上::域限定符
对象的建立和使用
C++中的new和delete关键字
在C语言中,动态分配内存用malloc
函数,释放内存用free
函数,在C++中依然可以使用,但是C++中新增了俩个关键字new
和delete
, new
用来动态分配内存,delete
用来动态释放内存
用new和delete分配内存更加简单
int *a= new int;//分配一个int类型的内存空间;
delete a;//释放内存;
//分配10个int数据空间
int *aa= new int[10];
delete []aa;
和malloc
函数一样,new也是在堆区分配内存,必须手动释放,否则只能等到程序运行结束才可以释放,为了避免内存泄露,new
和delete
,new[]
和delete[]
操作成对出现,并且不要和C语言中的malloc
和free
混用
C++中建议使用new和delete,因为C++中加入和很多新特性
1 对象的创建
Status st;
st.a=1;
st.socre=100;
strcpy(st.name, "wang");
st.print();
2 对象指针
Sts *p;
Sts s;
s.a=100;
s.socre=200;
strcpy(s.name, "xin");
p=&s;
p->print();
指针可以通过->来访问成员变量和方法
传递参数的时候建议使用指针来传递,因为传递的是地址,不会进行对象间的副本赋值,从而提高效率,减少内存开销
3 对象引用
引用C++中的新的类型,对象的引用是一个对象的别名,本质上也是把对象的地址赋值给了这个引用类型,俩者指向同一块内存空间
定义
Student A;
Student &Aq=A;
定义一个Student的对象,然后用&符号定义一个该类型的引用类型,并把A对象赋值给Aq初始化
需要注意的是:
- 与指针一样,需要同类型才可以赋值
- 除非做函数的返回值或形参,其余定义引用类型的同时就要初始化
- 引用类型并不是建立一个新的对象,因此不会调用构造函数
使用
Student A;
Student &Aq=A;
Aq.print();
其使用跟对象是一样的方式
我们可以看到,引用其实也是地址,传参不会耗太多内存,有指针的优势,同时使用起来和对象本身使用起来一样,再做函数实参是,直接传入即可,不用加地址符,看起来更加的直观方便,这就是引用类型的优点
#include <iostream>
#include <string>
using namespace std;
class Status{
public:
int a;
char name[100];
int socre;
int print(){
cout<<a<<"/"<<name<<"/"<<socre<<endl;
return 0;
}
};
class Sts{
public:
int a;
char name[100];
int socre;
int print();
};
int Sts::print(){
cout<<a<<"/"<<name<<"/"<<socre<<endl;
return 0;
}
int main(int argc, const char * argv[]) {
Status st;
st.a=1;
st.socre=100;
strcpy(st.name, "wang");
st.print();
Sts *p;
Sts s;
s.a=100;
s.socre=200;
strcpy(s.name, "xin");
p=&s;
p->print();
Sts &Aq=s;
Aq.print();
return 0;
}
输出
1/wang/100
100/xin/200
100/xin/200
C++的构造函数
C++的构造函数和java的构造函数功能大致相同,当我们定义一个类对象时,系统会自动调用它,进行专门的初始化,如果我们没有定义构造函数,则系统会自动生成一个无参构造函数,如果我们自己定义了,则系统不再自动生成构造函数,会区配用户自定义的构造函数,
#include <iostream>
#include <cstring>
using namespace std;
class Student {
public:
int a ;
char name[100];
int socre;
Student (int num , char *str,int sor);
void print();
};
Student::Student(int num, char *str,int sor){
a=num;
strcpy(name, str);
socre=sor;
cout<<"构造函数"<<endl;
}
void Student::print(){
cout<<a<<"/"<<name<<"/"<<socre<<endl;
}
int main(int argc, const char * argv[]) {
Student stu(100,"heihei",200);
stu.print();
return 0;
}
输出
构造函数
100/heihei/200
C++中的析构函数
对象创建时会调用构造函数,对象销毁时会调用析构函数,他也和类同名,也没有返回值,名字前有一个波浪线~,来区分构造函数,它主要做对象释放后的清理工作
如果用户不定义,则系统会自动生成一个,如果用户定义,则会在对象销毁时自动调用
构造函数可以重载,但是析构函数不可以重载,但他可以是虚函数,一个类只能有一个析构函数
#include <iostream>
#include <cstring>
using namespace std;
class Status{
public:
int a ;
char name[100];
int sorce;
Status(int a ,char *str,int sor);
~Status();
};
Status::Status(int a,char *str,int sor){
this->a=a;
strcpy(name, str);
sorce=sor;
cout<<this->a<<name<<sorce<<endl;
}
Status::~Status(){
cout<<"析构函数"<<endl;
}
int main(int argc, const char * argv[]) {
Status sta(1,"aaa",200);
Status stw(10,"aaaq",2001);
return 0;
}
输出
1aaa200
10aaaq2001
析构函数
析构函数
C++中的拷贝构造函数
在C++中,与类同名,且形参是本类对象的引用类型函数,叫做拷贝构造函数,与构造函数一样,如果我们不主动定义,那么系统会自动生成一个,进行俩个对象成员之间的赋值,用来初始化一个对象
#include <iostream>
#define AA 3.999
using namespace std;
class Status{
public:
int a ;
int b ;
Status(int a,int b);
Status(Status &A);
};
Status::Status(int a,int b){
this->a=a;
this->b=b;
cout<<a<<"/"<<b<<endl;
}
Status::Status(Status &A){
this->a=A.a;
this->b=A.b;
cout<<"拷贝k函数"<<a<<"/"<<b<<endl;
}
int main(int argc, const char * argv[]) {
Status st(1,2);
Status sts(st);
return 0;
}
输出
1/2
拷贝k函数1/2
第一次定义的对象是用带参的构造函数,第二次定义的对象,使用第一次的对象来进行初始化所以调用拷贝构造函数
浅拷贝和深拷贝
上面讲到的拷贝函数和系统的操作方式一样,那么为什么我们要重写一遍呢?我们看下个例子
#include <iostream>
#include <cstring>
using namespace std;
class Status{
public:
int a;
int b;
char *str;
Status(int a,int b,char *str);
~Status();
void print();
};
Status::Status(int a,int b,char *str){
this->a=a;
this->b=b;
this->str=new char[strlen(str)+1];
strcpy(this->str, str);
}
Status::~Status(){
delete []str;
}
void Status::print(){
cout<<a<<"/"<<b<<"/"<<str<<endl;
}
int main(int argc, const char * argv[]) {
char *pp="2we";
Status st(1,2,pp);
st.print();
Status ss(st);
ss.print();
return 0;
}
这段代码执行起来会有问题,原因是,默认拷贝函数,只是this.str->str
简单的赋值,俩个对象指向了同一个地址,并没有为新的对象开辟新的空间,这已经违背了我们的初衷,俩个对象销毁时都会调用析构函数,释放这段代码的内存空间,由于对象调用俩次,delete了俩次,就会报错
浅拷贝和深拷贝(47128,0x1000ac5c0) malloc: *** error for object 0x100595040: pointer being freed was not allocated
所以当类中有指针类型,依靠默认的拷贝方发,已经不能满足我们的需求,定义一个特定的拷贝构造函数,不仅进行数据的拷贝,还要为成员分配空间,这就叫做深拷贝,也就是深拷贝构造函数
#include <iostream>
#include <cstring>
using namespace std;
class Status{
public:
int a;
int b;
char *str;
Status(int a,int b,char *str);
~Status();
Status(Status &A);
void print();
};
Status::Status(int a,int b,char *str){
this->a=a;
this->b=b;
this->str=new char[strlen(str)+1];
strcpy(this->str, str);
}
Status::~Status(){
delete []str;
}
Status::Status(Status &A){
this->a=A.a;
this->b=A.b;
this->str=new char[strlen(A.str)+1];
strcpy(this->str, A.str);
}
void Status::print(){
cout<<a<<"/"<<b<<"/"<<str<<endl;
}
int main(int argc, const char * argv[]) {
char *pp="2we";
Status st(1,2,pp);
st.print();
Status ss(st);
ss.print();
return 0;
}
友元函数
我们知道类内的私有变量,只能在类内访问,类外不能访问,假如我们需要再类外访问私有变量,那我们该怎么办呢?
我们可以利用友元函数,把外部的函数声明为友元类型,赋予它可以在类外访问类的私有变量
友元函数既可以是全局函数,也可以是其他类的成员函数,不仅如此,友元还可以是一个类,叫做友元类
怎么使用?
对于友元函数只需要在类中对函数进行声明,并在之前加上friend
关键字,这个函数就有了独特的权限
需要注意的是,友元函数并不属于类,不可以用this指针,同时也不可以被继承
#include <iostream>
using namespace std;
class Status{
private:
int a;
int b;
public:
Status(int a,int b){
this->a=a;
this->b=b;
};
friend void print(Status st);
};
void print(Status st){
cout<<st.a<<"/"<<st.b<<endl;
}
int main(int argc, const char * argv[]) {
Status st(1,2);
print(st);
return 0;
}
输出
1/2
友元类
友元类和友元函数一样,如下
#include <iostream>
using namespace std;
class Status{
private:
int a;
int b;
public:
Status(int a,int b){
this->a=a;
this->b=b;
};
friend class A;
};
class A{
public:
void print(Status &st){
cout<<st.a<<"/"<<st.b<<endl;
}
};
int main(int argc, const char * argv[]) {
Status st(1,2);
A a;
a.print(st);
return 0;
}
输出
1/2
C/C++中static关键字作用
下面介绍一下C和C++的static关键字的区别
C中static的作用
- 1 静态全局变量
在全局变量前加static就是静态全局变量
静态全局变量默认初始化为0,知道程序结束才销毁
静态全局变量在该文件内可见,在其他文件不可见(普通的全局变量可以被其他文件可见)
- 局部静态变量
局部变量前加上static就是静态局部变量
静态局部变量只在首次执行到声明处初始化一次,之后再次执行到该语句不在执行初始化,如没有初始化则默认初始化为0;直到程序结束才销毁
- 静态函数
在普通函数前面加上static关键字就是静态函数
只能在本文件内使用
C++中的static 与java的static有些类似
- 静态数据成员
在类内的成员变量加上static关键字就是,静态成员函数
无论对少个对象,只存在一份拷贝,该静态成员被所有该类的对象共享
静态成员函数只能在类中声明,不能再类中初始化,只能在类外初始化
静态成员初始化的基本形式为, <类型名> <类名>::<变量名> = <值>
静态成员函数可以直接用类名加作用域运算符(::)调用 <类名>::<变量名>
- 静态成员函数
在普通成员函数加上static
关键字,即为静态成员函数
在类外定义静态成员函数时,不需要加static ,只要在类中声明时加上即可
静态成员函数只能访问静态数据成员和静态成员函数,普通成员函数可以访问静态成员函数和静态成员数据
可以使用<类名>::<函数名> 访问,也可以使用对象(./->)访问
#include <iostream>
using namespace std;
class A{
public:
static int a;
int b=2;
static void print();
};
int A::a=3;
void A::print(){
cout<<a<<"/"<<endl;
};
int main(int argc, const char * argv[]) {
cout<<A::a<<"/"<<endl;
A::print();
return 0;
}
C++中常数据使用及其初始化
被关键字const
修饰的变量,是不可修改和改变的,他除了可以修饰变量还可以修饰对象为常对象,修饰类的成员和成员函数,分别叫做类的常数据成员和常成员函数
常数据成员
使用格式
数据类型 const 数据成员名;
或 const 数据类型 数据成员名;
被const修饰的变量必须初始化并且不可以被修改
而初始化的方式是在构造函数中初始化,C++11支持后支持直接初始化
另外一种情况如果是static变量,初始化需要在类外初始化
例子如下
#include <iostream>
using namespace std;
class A{
public:
//可以直接初始化
const int a = 0;
const int b;
const int c;
static int d;
//静态常量可以在类内初始化
static const int e = 4;
//在构造方法初始化
A(int b,int c):b(b),c(c){
}
void print(){
cout<<a<<"/"<<b<<"/"<<c<<"/"<<d<<"/"<<e<<"/"<<endl;
}
};
//静态变量再类外初始化
int A::d =3;
int main(int argc, const char * argv[]) {
A a(1,2);
a.print();
return 0;
}
输出
0/1/2/3/4/
常对象
C++中可以把一个对象声明成const
类型,成为常对象,这样声明后,这个对象整个声明周期都不可以被更改,所以定义的时候需要在构造函数初始化;
定义的格式如下
类型 const 对象名;
或 const 类型 对象名;
需要注意的是常对象只能访问,常成员函数,不能访问非常成员函数
实例
#include <iostream>
using namespace std;
class A{
public:
const int a;
int b;
A(int a):a(a){
b=2;
}
void print(){
}
void print1() const{
cout<<a<<"/"<<b<<endl;
}
};
int main(int argc, const char * argv[]) {
const A a(1);
const A a1(2);
//编译失败,常对象不能被赋值
// a=a1;
//此处编译失败,常对象不能调用非常成员函数
// a.print();
a.print1();
return 0;
}
常成员函数
一个类中的成员函数被const
修饰后,就变成了常成员函数
定义如下
返回类型 函数名(形参表列)const;
需要注意的是
- 常成员函数的定义和声明都需要包含
const
- 长城园函数只能调用常成员函数,而不能调用非常成员函数,可以访问但不可以更改非常成员变量
#include <iostream>
using namespace std;
class A{
public:
const int a;
int b;
A(int a):a(a){
b=2;
}
void print(){
}
void print1() const{
//此处编译错误,常成员函数不可以改变非常成员变量
// b=3;
//此处编译错误,常成员函数不可以调用非常成员变量
// print();
cout<<a<<"/"<<b<<endl;
}
};
int main(int argc, const char * argv[]) {
const A a(1);
const A a1(2);
//编译失败,常对象不能被赋值
// a=a1;
//此处编译失败,常对象不能调用非常成员函数
// a.print();
a.print1();
return 0;
}
继承和派生
C++继承和java继承概念相同,我们看下如何实现继承
#include <iostream>
using namespace std;
class A{
public:
int a;
void setA(int a){
this->a=a;
}
void showA(){
cout<<a<<endl;
}
};
class B:public A{
public:
int b;
void setB(int b){
this->b=b;
}
void showB(){
cout<<b<<endl;
}
};
int main(int argc, const char * argv[]) {
B b;
b.setA(1);
b.setB(2);
b.showA();
b.showB();
return 0;
}
输出
1
2
三种继承方式
一共有三种继承方式,分别是,公有继承,私有继承,保护继承,下面分别介绍下
公有继承
- 基类中公有成员,在派生类中也是公有成员,无论是派生类的成员函数还是派生类对象都可以访问
- 派生类的私有成员,无论是派生类的成员函数还是派生类的对象都不可以访问
- 派生类的保护成员,在派生类中依然是保护成员,可以通过派生类的成员函数访问,派生类对象不可以访问
私有继承
- 基类中的公有和保护类型,被派生类私有继承后,都变为派生类的私有类型,即在类的成员函数可以访问,但是不能再类外访问
- 基类的私有成员,在派生类无论是类外还是类内都不可以访问
可以看出如果为私有继承,则私有成员在派生类中都不可以使用,没有什么作用,这种情况使用较少
保护继承
- 基类的公有成员和保护类型成员在派生类中为保护类型成员
- 基类的私有成员在派生类中不可以直接访问
- 派生类的成员函数可以访问基类的公有成员和保护成员,但是在类外通过派生类的对象则无法访问他们,同样无论是派生类的成员函数还是对象都不可以访问基类的私有成员
公有继承 | 保护继承 | 私有继承 | ||||
---|---|---|---|---|---|---|
访问位置 | 类内 | 类外 | 类内 | 类外 | 类内 | 类外 |
公有成员 | 可以 | 可以 | 可以 | 不可以 | 可以 | 不可以 |
保护成员 | 可以 | 不可以 | 可以 | 不可以 | 可以 | 不可以 |
私有成员 | 不可以 | 不可以 | 不可以 | 不可以 | 不可以 | 不可以 |
派生类的构造函数
我们在创建一个派生类时,系统会首先创建一个基类,派生类会吸收所有基类的成员,但是不会吸收构造函数和析构函数,那么早调用派生类的构造函数之前,会先调用基类的构造函数,当基类的构造函数是带参数的,那么派生类就要明确指出父类的构造函数并且指定参数
#include <iostream>
using namespace std;
class A{
public:
int a;
A(){
cout<<"父类的无参构造函数"<<endl;
}
A(int a){
this->a=a;
cout<<"父类的有参构造函数"<<endl;
}
};
class B:public A{
public:
B(){
cout<<"子类的无参构造"<<endl;
}
//指定父类带参构造和参数
B(int a):A(a){
cout<<"子类的有参构造函数"<<endl;
}
};
int main(int argc, const char * argv[]) {
B b;
B b1(1);
return 0;
}
输出
父类的无参构造函数
子类的无参构造
父类的有参构造函数
子类的有参构造函数
派生类的析构函数
基类的析构函数也不可能以被继承,其调用顺序为,子类析构函数->基类析构函数,和构造的函数的调用顺序相反
#include <iostream>
using namespace std;
class A{
public:
~A(){
cout<<"父类的析构函数"<<endl;
}
};
class B:public A{
public:
~B(){
cout<<"子类的析构函数"<<endl;
}
};
int main(int argc, const char * argv[]) {
B b;
return 0;
}
输出
子类的析构函数
父类的析构函数
虚基类和虚基类的使用
我们看下多继承的情况
#include <iostream>
using namespace std;
class A{
public:
int b;
};
class B:public A{
};
class C:public A{
};
class D :public B,public C{
};
int main(int argc, const char * argv[]) {
D d;
//在这里编译报错,D继承了俩份b变量
// d.b=8;
return 0;
}
解决这种情况需要虚基类
所谓的虚基类就是在继承的public之前加上virtual关键字,只要加上这个关键字派生类就维护一份基类对象,避免多次拷贝,出现歧义
使用
#include <iostream>
using namespace std;
class A{
public:
int b;
};
class B:virtual public A{
};
class C:virtual public A{
};
class D :public B,public C{
};
int main(int argc, const char * argv[]) {
D d;
d.b=8;
return 0;
}
多态的概述
指同样的方法被不同的对象执行时会有不同的效果
多态的实现又分为俩种,编译时多态和运行时多态,前者是编译时就确定了操作过程,后者是程序运行之中才确定了操作过程,这种操作过程就是联编,也称为绑定
联编在编译链接时确认的,叫做静态联编,之前遇到的函数重载和函数模板就属于这一类
在运行时才确定执行那段代码的,就做动态联编
静态联编在编译时就确定说以效率比较高,动态联编虽然慢些但比较灵活,各有优点
静态联编的例子
#include <iostream>
using namespace std;
class A{
public:
void print(){
cout<<"A"<<endl;
}
};
class B:public A{
public:
void print(){
cout<<"B"<<endl;
}
};
int main(int argc, const char * argv[]) {
A a;
a.print();
B b;
b.print();
A *p;
p=&b;
p->print();
A &pp=b;
pp.print();
return 0;
}
输出
A
B
A
A
这个是静态联编,在编译时已经确定了*p和&pp的类型为A,所以输出为A
很明显这不是我们期望的结果,如果想要达到我们的要求,无论指针和引用是什么类型,都要以实际指向的对象灵活决定,那么我们就要改变默认的静态联编,采用动态联编
虚函数
什么是虚函数
在函数前面加上virtual
关键字,就是虚函数
virtual 函数返回值 函数名(形参)
{
函数体
}
有什么作用
虚函数的出现允许函数,在调用时与函数体的联系在运行时建立,就是所谓的动态联编,那么在虚函数的派生类在运行的时候,就可以根据动态联编实现执行一个方法,却又不同的效果,这就是多态,可以解决上节遇到的问题
我们只需要把基类中的函数变成虚函数就可以了,如下
#include <iostream>
using namespace std;
class A{
public:
virtual void print(){
cout<<"A"<<endl;
};
};
class B : public A{
public:
void print(){
cout <<"B"<<endl;
}
};
int main(int argc, const char * argv[]) {
A a;
a.print();
B b;
b.print();
A *p;
p=&b;
p->print();
A &pp=b;
pp.print();
return 0;
}
输出
A
B
B
B
这次的输出就符合我们的期望,这就是多态
需要注意的是
- 虚函数不能是静态成员函数,或友元函数,因为他们不属于某个对象
- 内联函数不能运行中动态指定其位置,及时虚函数在类内定义,编译时,仍将看做非内联
- 构造函数不能是虚函数,析构函数可以是虚函数,而且通常声明为虚函数
虚析构函数
在C++中,不能把构造函数定义为虚构函数,因为在实例化一个对象时才会调用构造函数,切虚函数的实现本质上是通过一个虚拟函数指针表来调用的,还没有对象,没有内存空间当然无法调用
但析构函数可以是虚函数,且大多数时候都声明为虚构函数,这样就可以用基类的指针,指向派生类对象在释放时,可以根据实际指向的对象类型,动态联编子类的析构函数,来实现正确的内存释放
#include <iostream>
using namespace std;
class A{
public:
char *str;
A(){
this->str=new char[100];
cout<<"A的构造函数"<<endl;
}
~A(){
delete []str;
cout<<"A的析构函数"<<endl;
}
};
class B : public A{
public:
char *str;
B(){
this->str=new char[100];
cout<<"B的构造函数"<<endl;
}
~B(){
delete [] str;
cout<<"B的析构函数"<<endl;
}
};
int main(int argc, const char * argv[]) {
A *p;
p=new B();
delete p;
return 0;
}
输出
A的构造函数
B的构造函数
A的析构函数
我们可以看到基类中的析构函数没有加virtual
,且基类和派生类中都有动态的内存开辟,当时只执行了基类的析构函数,不能正确的释放内存
#include <iostream>
using namespace std;
class A{
public:
char *str;
A(){
this->str=new char[100];
cout<<"A的构造函数"<<endl;
}
virtual ~ A(){
delete []str;
cout<<"A的析构函数"<<endl;
}
};
class B : public A{
public:
char *str;
B(){
this->str=new char[100];
cout<<"B的构造函数"<<endl;
}
~B(){
delete [] str;
cout<<"B的析构函数"<<endl;
}
};
int main(int argc, const char * argv[]) {
A *p;
p=new B();
delete p;
return 0;
}
输出
A的构造函数
B的构造函数
B的析构函数
A的析构函数
这样就可以正确的释放内存
纯虚函数和抽象类
纯虚函数就是没有方法体的函数
virtual 返回值 函数名(形参)=0;
前面和虚函数一样,后面加一个=0,表示没有函数体,包括纯虚函数的类就是抽象类,一个抽象类至少有一个纯虚函数
抽象类的特点如下
- 抽象类无法实例出一个对象,只能作为基类让派生类去实现抽虚函数,然后再实例化使用
- 抽象的派生类依然不可以实现基类中的纯虚函数,继续作为抽象类被派生
- 抽象类因为抽象无法具化,所以不能作为参数类型返回值,强转类型
- 但抽象类可以定义一个指针,引用,指向派生类来实现多态性
C++中的异常处理
C++的异常处理和java的差不多都是用到了try,catch,throw
三个关键字
举个例子
#include <iostream>
using namespace std;
int main(int argc, const char * argv[]) {
int a =0;
int b=1;
cout<<a<<"/"<<b<<endl;;
try {
if (a==0) {
throw "a为0异常";
}
} catch (const char *str) {
cout<<str<<endl;
}
return 0;
}
输出
0/1
a为0异常
C++中标准异常处理类
可以看到所有的异常都是基于exception
基类,他下面有俩大子类logic_error
和runtime_error
分别代表逻辑异常和运行时异常
举例说明:
- 当我们new一个对象,内部不足,会抛出
bad_alloc
异常 - 当我们使用string类,下标越界会抛出
out_of_range
异常
注意使用C++自带的异常类需要导入对应的头文件
bad_alloc类在头文件new中定义
bad_typeid类在头文件typeinfo中定义
ios_base::failure类在特批文件ios中定义
其他异常类在stdexcept中定义
例子如下:
#include <iostream>
#include <new>
#include <stdexcept>
using namespace std;
int main(int argc, const char * argv[]) {
string *s;
try {
s = new string("123456");
cout<<s->substr(7,3);
} catch (bad_alloc &t) {
cout<<"异常:"<<t.what()<<endl;
}catch(out_of_range &t){
cout<<"异常:"<<t.what()<<endl;
}
return 0;
}
输出
异常:basic_string
C++读写文件操作
C++中对于文件的操作主要基于以下类
ofstream写操作(输出)文件类(由ostream引申而来)
ifstream读操作(输入)文件类(由istream引申而来)
fstream可同时读写文件类(由iostram引申而来)
他们都需要包含头文件
#include <fstream>
C++如何打开文件
这里需要用到fstream的open函数,实现打开文件的操作,他的原型如下
void open(const char *filename, ios::openmode mode);
第一个参数表示打开文件的路径
第二参数表示打开的模式
参数 | 作用 |
---|---|
ios::in | 为输入(读)而打开文件 |
ios::out | 为输出(写)而打开文件 |
ios::ate | 初始位置:文件尾 |
ios::app | 所有输出附加在文件末尾 |
ios::trunc | 如果文件已存在则先删除该文件 |
ios::binary | 二进制方式 |
除此之外还可以通过|
符号将多个参数进行使用
1 ofstream out;
2 out.open("dotcpp.txt", ios::out|ios::binary) //以二进制模式打开,进行写数据
C++读文件的操作
C++输入输出用到了iostream头文件代表io流,这次学习的读文件,尤其是流,文件流,要包含fstream头文件,它定义了三个类分别负责,读,写,读写操作
类型 | 意义 |
---|---|
ofstream | (out) 表示输出文件流,用于创建文件并向文件写入信息。 |
ifstream | (in)表示输入文件流,用于从文件读取信息。 |
fstream | (file)表示文件流,且同时具有 ofstream 和 ifstream 两种功能,这意味着它可以创建文件,向文件写入信息,从文件读取信息。 |
读文件例子
#include <iostream>
#include <fstream>
using namespace std;
int main(int argc, const char * argv[]) {
char data[100];
ifstream in;
in.open("/Users/renxiaohui/Desktop/test.txt");
in>>data;
cout<<data<<endl;
in.close();
return 0;
}
读取已存在的文件
输出
我是demo
C++写文件
#include <iostream>
#include <fstream>
using namespace std;
int main(int argc, const char * argv[]) {
const char *name="测试测试测试";
ofstream out;
out.open("/Users/renxiaohui/Desktop/test1.txt");
out<<name;
out.close();
return 0;
}
如果没有这个文件的话就会自动创建一个新文件
参考:
https://www.dotcpp.com/course/82