C++基础 原创
C++基础
更多精彩内容 |
---|
👉个人内容分类汇总 👈 |
1、取余
-
C++ double型不能实施**%操作符,作为除数被除数都不可以,但可以用fmod函数**,则作为除数被除数都可以
-
头文件:
#include <math.h>
-
double fmod(double x, double y) 该函数返回 x/y 的余数。
-
double fmod (double numer , double denom); float fmod (float numer , float denom); long double fmod (long double numer, long double denom);
-
2、变量
- 变量存在的意义
- 方便我们管理内存空间
1字符串
-
字符串拼接
-
string + string
string a = "123"; string b = "456"; a += b;
-
string + char
string a = "123"; char b = '4'; a += b;
-
3、常量
- 作用: 用于记录程序中不可更改的数据
1 定义常量的方法
- #define 宏常量
- 通常在文件上方定义,表示一个常量
- const修饰的变量
- 通常在变量定义前加关键字const,修饰该变量为常量,不可修改
4、sizeof 关键字
- 作用: 利用sizeof关键字可以统计数据类型所占内存大小
5、实型
数据类型 | 占用空间 | 有效数字范围 |
---|---|---|
float | 4字节 | 7位有效数字 |
double | 8字节 | 15 ~ 16 位有效数字 |
-
默认情况下,输出一个小数,会显示6位有效数字
-
在创建float类型变量时需要在后面加上f
float f1 = 3.14f; // 如果不加 f 会默认3.14是double类型,然后再转换为float类型
6、程序流程结构
- C/C++支持最基本的三种程序运行结构:顺序结构、选择结构、循环结构
7、运算符
-
三目运算符
-
在C语言中,三目运算符得到的结果不能作为左值。用C语言编译器在编译的时候,三目运算符传入的是值而不是变量。
-
在C++中,是允许三目运算符作为左值的,三目运算符返回的是变量,它返回的其实是a或者b的引用,也就是a或者b的别名,代表的是相同的一段存储空间,而不是值,可以继续赋值。
-
但是三目运算符作为左值使用是有条件的,就是三目运算符中返回的可能值如果有常量,则就会出现编译错误,也就是说,这个时候的三目运算符不能作为左值使用。如:
int a = 10; int b = 20; (a > b ? a : b) = 100; cout << a << b<< endl; 打印 10 100
-
8、数组
-
如果在初始化数据的时候,没有全部填写完,会自动用0来填补剩余数据。如:
int a[10] = {1,2,3} 打印结果为 1 2 3 0 0 0 0 0 0 0
9、函数
- 函数中的参数只有在调用的时候才分配内存
1 常见函数样式有4种
-
无参无返
void test()
-
有参无返
void test(int a)
-
无参有返
int test()
-
有参有返
int test(int a)
2 函数的分文件编写
-
作用: 让代码结构更加清晰
-
步骤:
- 创建.h头文件
- 创建.cpp源文件
- 在头文件中写函数的声明
- 在源文件中写函数的定义
-
使用:
-
hello.cpp
#include <iostream> #include "hello.h" //加入这行可会将当前源文件所有函数的声明加入当前位置,可防止调用函数顺序问题,如hello1调用hello using spacename std; void hello1(){ hello(); } void hello(){ cout <<"hello" << endl; }
-
hellp.h
- 使用.h文件可以不用每次使用其他文件的函数时还需要声明
void hello(); void hello1();
-
main.cpp
#include <iostream> #include "hello.h" using spacename std; int main() { hello1(); }
-
3 函数默认参数
-
在C++中,函数的形参列表中的形参是可以有默认值的。
-
语法:
返回值类型 函数名 (参数 = 默认值)
int func(int a, int b = 10, int c = 20) { return a + b +c; }
-
注意:
- 若某个位置有了默认参数,那么从这个位置往后,从左到右都要有默认参数。
- 默认参数可以放在函数声明或者定义中,但只能放在二者之一,否则会有二义性。通常放在声明中
int func(int a = 10, int b , int c = 20) //错误
4 函数占位参数
-
C++中函数的形参列表里可以有占位参数,用来做占位,调用函数时必须填补该位置。
-
语法:
返回值类型 函数名 (数据类型){}
void func(int a, int) { cout << a<<endl; } func(10, 10);
-
占位参数可以有默认参数
void func(int a, int = 10){}
10、指针
- 作用: 可以通过指针间接访问内存
- 大小: 指针是一种数据类型
- 32位系统下指针都为4个字节
- 64位系统下指针都为8个字节
1 空指针
- 空指针: 指针变量指向内存中编号为0的空间
- 用途: 初始化指针变量
- 注意: 空指针指向的内存是不可访问的
2 野指针
- 野指针: 指针指向非法的内存空间
3 const 修饰指针
-
const修饰指针有三种情况
-
const修饰指针:常量指针
int a = 10; int b = 20; const int* p = &a; //const修饰的是int * ,表示内容是常量,地址可以改,内容不可改 p = &b ;
-
const修饰常量:指针常量
int * const p1 = &a; //const修饰的是p1, 表示p1指向的内存是常量,内容可以改,地址不可改 *p1 = 30;
-
const既修饰指针又修饰常量
const int* const p2 = &a; //内容和地址都不可改
-
11、结构体
-
结构体属于用户自定义的数据类型,允许用户存储不同的数据类型
-
在C++中,定义结构体变量时struce关键字可以省略,在C语言中不可省略,如:
//声明结构体student 声明定义在一起,必须在main前 struct student { int id; string name; }; int main() { student a = {1, "张三"}; cout << a.id << a.name<<endl; return 0; }
1 结构体数组
- 作用: 将自定义的结构体放入数组中,方便维护
- 语法:
struct 结构体名 数组名[个数] = { {}, {}, {}, ..., {}};
2 结构体做函数参数
- 作用: 将结构体作为参数向函数中传递
- 传递方式:
- 值传递
- 安全,不会修改原数据
- 当结构体内变量多时,拷贝的数据量大,消耗内存大
- 地址传递
- 不用拷贝,速度快,内存消耗小
- 不安全,需要防止修改
- 值传递
3 结构体中const使用场景
-
作用: 用const来防止误操作
- 因为使用值传递将结构体传入函数时需要拷贝所有数据,当结构体数据多时则拷贝速度慢,消耗双倍内存,所以可以选择使用地址传递,这时就需要使用const关键字来防止误操作
-
如:
struct student { int id; string name; }; void print(const student *a) { cout << a->id << a->name<< endl; } int main() { student a = {1, "张三"}; print(&a); return 0; }
12、内存四区
- 内存分区模型
- 代码区: 存放函数体的二进制代码,由操作系统进行管理
- 全局区: 存放全局变量和静态变量以及常量
- 栈区: 由编译器自动分配释放,存放函数的参数值,局部变量
- 堆区: 由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收
- 意义: 不同区域存放的数据,赋予不同的生命周期,使编程更灵活
1 程序运行前
- 在程序编译后,生成可执行程序,未执行程序前分为两个区域
- 代码区:
- 存放CPU执行的机器指令
- 代码区是共享的,共享的目的是对于频繁被执行的程序,只需要在内存中有一份代码即可
- 代码区是只读的,使其只读的原因是防止程序意外修改了它的指令
- 全局区:
- 全局变量和静态变量存放在此
- 全局区还包含了常量区,字符串常量和其他常量也存放在此
- 该区域的数据在程序结束后由操作系统释放
2 程序运行后
- 栈区:
- 由编译器自动分配释放,存放函数的参数值,局部变量等
- 注意事项:不要返回局部变量的地址,栈区开辟的数据由编译器自动释放
- 堆区:
- 由程序员分配释放,若程序员不释放,程序结束时由操作系统回收
- 在C++中主要利用new在堆区开辟内存
3 new操作符
- C++中利用new 操作符在堆区开辟数据
- 堆区开辟的数据,由程序员手动开辟,手动释放,释放利用操作符delete
- 利用new创建的数据,会返回该数据对应的类型指针
4 delete
- delete释放数组时需要加[]
delete [] arr;
- 注意: delete后只是释放了堆中的空间,指针p仍然指向之前的位置,成为野指针,所以可以访问,但这时p指向的位置已经被回收了,随时可能分配给其它对象,所以在delete后最好将指针置为nullptr,可防止野指针。
5 NULL 和nullptr
- NULL是一个宏定义,在c和c++中的定义不同,c中NULL为(void*)0,而c++中NULL为整数0,是int型
- 所以在c++中int *p=NULL; 实际表示将指针P的值赋为0,而c++中当一个指针的值为0时,认为指针为空指针
- nullptr是一个字面值常量,类型为std::nullptr_t,空指针常数可以转换为任意类型的指针类型。
- 在c++中(void *)不能转化为任意类型的指针,即 int p=(void)是错误的,但int *p=nullptr是正确的
13、引用
1 基本使用
-
作用: 给变量起别名
-
优点:
- 功能性:可以满足指针的多数需要使用指针的场合,
- 安全性:可以避开由于指针操作不当带来的内存错误
- 操作性:简单易用,又不失强大
-
基本语法
int a = 10; int &b = a
2 注意事项
- 引 用在定义时必须初始化, 指针没有要求。
- 一旦一个引 用被初始化为指向一个对象, 就不能再指向其他对象, 而指针可以在任何时候指向任何一个同类型对象。
- 没有NULL引 用, 但有NULL指针。
- 在sizeof中含义不同: 引 用结果为引 用类型的大小,但指针始终是地址空间所占字节个数。
- 引 用自 加改变变量的内 容, 指针自 加改变了 指针指向。
- 有多级指针, 但是没有多级引用。
3 引用做函数参数
-
作用: 引用是可以作为函数返回值存在的
-
注意: 不要返回局部变量引用
-
用法:
int& test() { static int a = 10; return a; } int &a = test();
-
如果函数的返回值是引用,则函数调用可以作为左值
test() = 20; //相当于返回变量a ,然后对a 赋值
4 引用的本质
- 本质: 引用的本质在C++内部实现是一个指针常量
- 从使用C++语言角度来看:
- 引用和指针没有任何关系;
- 引用是变量的新名字,操作引用就是操作对应的变量;
- 从C++编译器的角度来看:
- 为了支持新概念,引用;必须要有一个有效的解决方案;
- 在编译器内部,引用的实现是一个指针常量;
- 因此“引用”在定义时必须初始化;
5 常量引用
-
作用: 常量引用主要用来修饰形参,防止误操作
-
在函数形参列表中,可以加const修饰形参,防止形参改变实参
-
用法:
const int &b = 20; //没加const时不能在等号右边使用常量,加const相当于int a = 20; const int &b = a; //所以在引用做函数传参时使用const既可允许传入常量,也可以防止误操作 void test(int &a) { } void test1(const int &a) { } test(10); //如果传入常量则错误 test1(10); //加const则允许传入常量
14、重载
- 选择最合适的重载函数或重载运算符的过程,称为重载决策。
1 函数重载
-
作用: 函数名可以相同,提高复用性
-
函数重载满足条件:
- 同一个作用域下
- 函数名称相同
- 函数参数类型不同、个数不同或顺序不同
-
注意事项:
-
函数返回值不能作为函数重载的条件
-
引用作为重载条件
//引用作为重载条件 void func(int &a) { cout <<"1:"<<a <<endl; } void func(const int &a) { cout <<"2:" << a << endl; } int main() { int a = 10; func(a); //调用void func(int &a) func(10); //调用void func(const int &a) return 0; }
-
函数重载遇到默认参数
void func( int a) { cout <<"1:" << a << endl; } void func(int a, int b = 10) { cout << "2:" << a << endl; } int main() { func(10); //这种用出现二义性,会报错 func(10, 20); // 这种用发调用void func(int a, int b = 10) return 0; }
-
15、类和对象
-
C++面向对象三大特性: 封装、基础、多态
-
C++认为万事万物都为对象,对象上有其属性和行为,类中的属性和行为统称为成员
- 属性:成员属性、成员变量
- 行为:成员函数、成员方法
-
类的声明定义在一起,必须在main前
-
类的声明定义
//圆类 class circular { public: //属性 int m_r; //行为 double circularLong() { return 2 * PI * m_r; } }; int main() { circular c1; //实例化对象 c1.m_r = 10; cout << c1.circularLong() << endl; return 0; }
1 封装
- 封装是面向对象编程中的把数据和操作数据的函数绑定在一起的一个概念,这样能避免受到外界的干扰和误用,从而确保了安全
- 数据封装引申出了另一个重要的 OOP 概念,即数据隐藏。
- C++ 通过创建类来支持封装和数据隐藏(public、protected、private)
1.1 封装的意义:
- 将属性和行为作为一个整体,表现生活中的事物
- 类在设计时,可以把属性和行为放在不同的权限下,加以控制
1.2 访问权限:
- public:公共权限,成员类内可以访问,类外可以访问
- protected:保护权限,成员类内可以访问,类外不可以访问,继承时,子类可以访问
- private:私有权限,成员类内可以访问,类外不可以访问,子类不可以访问
1.3 成员属性设置为私有
- 优点1: 将所有成员属性设置为私有,可以自己控制读写权限
- 优点2: 对于写权限,我们可以检测数据的有效性
1.4 struct和class区别
- 在C++中struct和class唯一的区别就在于默认的访问权限不同
- struct 默认权限为公共
- class 默认权限为私有
2 对象的初始化和清理
2.1 构造函数和析构函数
-
对象的初始化和清理是两个非常重要的安全问题
- 一个对象或者变量没有初始化状态,对其使用后果是未知的
- 头像的使用完一个对象或者变量,没有及时清理,也会造成一定的安全问题
-
编译器会默认提供一个构造函数和一个析构函数,但是都是空实现。
-
构造函数:主要作用在于创建对象是为对象成员赋值,构造函数由编译器自动调用,无须手动调用
-
析构函数:主要作用在于对象销毁前自动调用,执行一些清理工作
-
构造函数语法:
类名 (){}
- 构造函数,没有返回值也不用写void
- 构造函数名称与类名相同
- 构造函数可以有参数,因此可以发生重载
- 程序在调用对象时候会自动调用构造,无须手动调用,而且只会调用一次
-
析构函数语法:
~类名(){}
- 析构函数,没有返回值,不写void
- 函数名与类名相同,在名称前加~
- 析构函数不可以有参数,一次不可重载
- 程序在对象销毁前会自动调用析构,无须手动调用,只会调用一次
2.2 构造函数的分类及调用
-
两种分类方式
- 按参数分:有参构造和无参构造
- 按类型分:普通构造和拷贝构造
-
三种调用方式
-
括号法
Person p1; //默认构造函数调用 Person p2(10); //有参构造 Person p3(p2); //拷贝构造
- 主要调用默认构造是,不要加(),如 Person p1(); 会被编译器认为是函数声明
-
显示法
Person p1; Person p1 = Person(); //调用默认构造或者无参构造 Person p2 = Person(10); //调用有参构造 Person p2 为匿名对象Person(10)定义一个名字 Person p3 = Person(p2); //调用拷贝构造
- 注意:
Person(10);
叫匿名对象,特点:在当前行执行结束后系统立即回收匿名对象Person(p);
不要用拷贝构造初始化匿名对象,编译器会认为是重定义Person p
- 注意:
-
隐式转换法
Person p = 10; //相当于Person p = Person(10); Person p1 = p;
-
2.3 拷贝构造函数调用时机
- C++中拷贝构造函数调用时机通常有三种情况
- 使用一个已经初始化完毕的对象来初始化一个新的对象
- 值传递的方式个函数参数传值
- 以值方式返回局部对象(这种方式不一定,可能是如
Person p = Person(10)
这种效果,没有调用拷贝构造)
2.4 构造函数调用规则
- 默认情况下,C++编译器至少给一个累添加三个函数
- 默认构造函数(无参,函数体为空)
- 默认析构函数(无参,函数体为空)
- 默认拷贝构造函数,对属性进行值拷贝
- 构造函数调用规则如下
- 如果用户定义了有参构造函数,C++不再提供默认无参构造,但会提供默认拷贝构造
- 如果用户定义拷贝构造函数,C++不会再提供其他构造函数
2.5 深拷贝与浅拷贝
- 浅拷贝: 简单的赋值拷贝操作
- 将一个对象的值用来初始化一个新的对象,如果是一般的对象默认拷贝足够使用了,但是当类持有其它资源时,例如动态分配的内存、指向其他数据的指针等,默认拷贝会直接将地址进行赋值,也就是新对象中的指针和用来的对象中的指针是指向同一片空间,在析构时delete指针,会造成重复释放(现在大部分编译器会对重复释放进行优化,并不会报错,但是如VS就会报错),所以我们必须显式地定义拷贝构造函数,以完整地拷贝对象的所有数据。
- 在部分编译器中,如g++,对象用对象p通过默认拷贝初始化对象p1,在将p.m_a用delete释放后,p1.m_a还是可以操作指向的内存空间的
- 默认拷贝构造就是浅拷贝
- 深拷贝: 在堆区从新申请空间,进行拷贝操作
2.6 初始化列表
-
作用:C++提供了初始化列表语法,用来初始化属性
-
语法:
构造函数():属性1,属性2,属性3...{}
Person():m_a(10), m_b(20), m_c(30) //无参构造初始化列表 { } Person(int a, int b, int c):m_a(a), m_b(b), m_c(c) //有参构造初始化列表 { }
2.7 类对象作为类成员
-
C++类中的成员可以是另一个类的对象,称该成员为对象成员。
-
如:
class A{} class B { A a; }
-
构造时:先A 后B
-
析构时:先B 后A
2.8 静态成员
-
静态成员就是在成员变量和成员函数前加上关键字static,称为静态成员
class Person { public: static void func() { cout <<"hello" << ":" << m_b<<endl; } int m_a; static int m_b; //静态成员变量 }; int Person::m_b = 0;
-
静态成员变量
- 所有对象共享同一份数据
- 在编译阶段分配内存
- 类内声明、类外初始化
-
静态成员函数
- 所有对象共享同一个函数
- 静态成员函数只能访问静态成员变量
-
访问方式
-
通过对象访问
-
通过类名访问
Person p; p.func(); //对象访问 Person::func(); //类名访问
-
3 C++对象模型和this指针
3.1 成员变量和成员函数分开存储
-
在C++中,类内的成员变量和成员函数分开存储
-
只有非静态成员变量才属于类的对象上
-
编译器会给每个空对象分配一个字节空间,是为了区分对象占内存的位置
//空类 class Person { }; //有一个成员变量 class Person1 { public: int m_a; }; //有一个静态成员变量 class Person2 { public: static int m_b; }; //有成员函数 class Person3 { public: void func() { cout <<"hello"<<endl; } static void func1() { cout <<"hello" << endl; } }; int main() { Person p; cout << sizeof(p) <<":"<<&p<<endl; //内存大小为1 Person1 p1; cout << sizeof(p1) <<":"<<&p1<<endl; //内存大小为4 Person2 p2; cout << sizeof(p2) <<":"<<&p2<<endl; //内存大小为1,说明静态成员变量不属于对象 Person3 p3; cout << sizeof(p3) <<":"<<&p3<<endl; /内存大小为1,说明成员函数和静态成员函数不属于对象 return 0; }
3.2 this指针
-
每一个非静态成员函数只会诞生一份函数实例,也就是说多个同类型的对象会共用同一块代码
-
C++通过提供特殊的对象指针,this指针,用来区别是哪个对象调用,this指针指向被调用的成员函数所属的对象
-
this指针是隐含每一个非静态成员函数中的一种指针
-
this指针不需要定义,直接使用即可
-
本质:this指针的本质是指针常量,指向不可修改
this =NULL; //错误
,指向的值可以修改this->a = 10
-
用途
-
当形参和成员变量同名是,可用this指针来区分
-
在类的非静态成员函数中返回对象本身,可使用*return this
class Person { public: Person(int a) { this->a = a; } void print() { cout <<a << endl; } Person& getPerson(const Person &p) //注意要返回引用,如果返回值则会创建一个新对象 { this->a += p.a; return *this; } int a; }; Person p(10); p.getPerson(10).getPerson(10); //链式编程思想 p.print(); //打印结果为30
-
3.3 空指针访问成员函数
-
C++中空指针也是可以调用成员函数的,但是要注意有没有用到this指针,如果用到this指针,需要加以判断保证代码的健壮性
class Person { public: void showName() { cout <<"hello" <<endl; } void showAge() { cout <<m_Age <<endl; } void showAge_1() { if(this == nullptr) { return; } cout <<m_Age <<endl; } int m_Age; }; Person *p = nullptr; p->showName(); //使用正确 p->showAge(); //错误,showAge中用到this指针 p->showAge_1(); //正确
3.4 const修饰成员函数
-
常函数
-
成员函数后加const后称为常函数
-
常函数内不可修改成员属性
-
成员属性声明时加关键字mutable后,在常函数中依然可以修改
class Person { public: void showPerson() const //如同:const Person *const this;表示this指针指向和 { //this指针指向的值的内容都不可以修改 this->m_A = 100; //错误 this->m_B = 100; //可以修改 } int m_A; mutable int m_B; };
-
-
-
常对象
-
声明对象前加const称为长对象
-
常对象只能调用常函数
- 因为常对象不能修改成员变量,而成员函数可以修改成员变量,防止常对象通过成员函数修改成员变量,所以常对象不能调用成员函数。
void test() { const Person p; p.m_A = 100; //错误 p.m_B = 100; //可以修改 }
-
4 友元
- 在程序中,有些私有属性也想让类外特殊的一些函数或者类进行访问,就需要用到友元技术
- 友元可以是一个函数,该函数被称为友元函数;友元也可以是一个类,该类被称为友元类,在这种情况下,整个类及其所有成员都是友元。
- 友元的目的就是让一个函数或者类访问另一个类中私有成员
- 关键字:friend
- 友元的三种实现
- 全局函数做友元
friend void good(Building &b);
- 类做友元
friend class Good;
- 成员函数做友元
friend void Good::visit();
- 全局函数做友元
4.1 全局函数做友元
class Building
{
friend void good(Building &b); //声明友元
public:
Building()
{
m_SittingRoom = "客厅";
m_BedRoom = "卧室";
}
public:
string m_SittingRoom;
private:
string m_BedRoom;
};
//全局函数做友元
void good(Building &b)
{
cout <<b.m_SittingRoom <<endl;
cout <<b.m_BedRoom <<endl; //访问对象私有成员
}
4.2 类做友元
class Building
{
friend class Good; //声明友元类
public:
Building()
{
m_SittingRoom = "客厅";
m_BedRoom = "卧室";
}
public:
string m_SittingRoom;
private:
string m_BedRoom;
};
class Good
{
public:
Good()
{
b = new Building;
}
void visit() //访问Building中的属性
{
cout <<b->m_SittingRoom <<endl;
cout <<b->m_BedRoom <<endl;
}
Building *b;
};
4.3 成员函数做友元
class Building;
class Good
{
public:
Good();
void visit(); //访问Building私有
Building * b;
};
class Building
{
friend void Good::visit(); //声明友元成员函数
public:
Building();
public:
string m_SittingRoom;
private:
string m_BedRoom;
};
//成员函数
Building::Building()
{
m_SittingRoom = "客厅";
m_BedRoom = "卧室";
}
Good::Good()
{
b = new Building;
}
void Good::visit()
{
cout <<b->m_SittingRoom <<endl;
cout <<b->m_BedRoom <<endl;
}
5 运算符重载
-
运算符重载: 对于已友运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型
-
关键字:operator
5.1 加号运算符重载
-
作用: 实现两个自定义数据类型相加运算
-
运算符重载也可以实现函数重载(重载多次运算符)
-
可通过成员函数重载,也可以通过全局函数重载
class Person { public: Person() { m_A = 10; m_B = 10; } //成员函数重载 Person operator+(Person &p) { Person temp; temp.m_A = this->m_A + p.m_A; temp.m_B = this->m_B + p.m_B; return temp; } int m_A; int m_B; }; //全局函数重载 Person operator+ (Person &p1, Person &p2) { Person temp; temp.m_A = p1.m_A + p2.m_A; temp.m_B = p1.m_B + p2.m_B; return temp; } void test() { Person p1; Person p2; //成员函数本质:Person p3 = p1.operator+(p2); //全局函数本质:Person p3 = operator+(p1, p2); Person p3 = p1 + p2; cout << p3.m_A <<":"<<p3.m_B <<endl; }
5.2 左移运算符重载
-
作用: 可以输出自定义数据类型
-
不会使用成员函数重载 <<运算符,因为无法实现cout 在左侧
-
一般重载为全局函数或类的友元函数,但考虑到重载运算符可能需要访问类的私有成员变量,所以一般声明为友元函数
class Person { public: Person() { m_A = 10; m_B = 20; } friend ostream & operator<<(ostream &put, const Person &p); private: int m_A; int m_B; }; //重载<< ostream & operator<<(ostream &put, const Person &p) { put << p.m_A <<" "<< p.m_B; return put; } void test() { Person p; cout << p <<endl; }
5.3 递增运算符重载
-
作用: 通过重载递增运算符,实现自己的整型数据
-
前缀形式重载调用 operator ++ () ,后缀形式重载调用 operator ++ (int)。
-
前置++返回引用,后置++返回值
class MyInteger { friend ostream & operator<<(ostream &put, const MyInteger &m); public: MyInteger() { m_Num = 0; } //前置++ MyInteger &operator++() //返回引用是为了一直对一个数据操作 { m_Num++; return *this; } //后置++ MyInteger operator++(int) //一定要返回值,因为M是局部对象 { //保存原始值 MyInteger M(*this); m_Num ++; return M; } private: int m_Num; }; //重载 << ostream & operator<<(ostream &put, const MyInteger &m) { put << m.m_Num; return put; } void test() { MyInteger Mi; cout << Mi++ <<endl; cout << Mi <<endl; }
5.4 赋值运算符重载
-
C++编译器至少给一个类添加4个函数
- 默认构造函数
- 默认析构函数
- 默认拷贝构造,对属性进行值拷贝
- 赋值运算符operator=,对属性进行值拷贝
-
如果类中有属性指向堆区,做赋值操作是也会出现深浅拷贝问题。
class Person { public: Person(int age) { m_Age = new int(age); } Person& operator=(const Person &p) { *m_Age = *p.m_Age; return *this; } ~Person() { delete m_Age; m_Age = nullptr; cout <<"析构" <<endl; } int *m_Age; }; void test() { Person p(18); Person p2(20); Person p3(30); cout <<p.m_Age <<":"<<*p.m_Age<<endl; cout <<p2.m_Age <<":"<<*p2.m_Age<<endl; p3 = p2 = p; cout <<p2.m_Age <<":"<<*p2.m_Age<<endl; cout <<p3.m_Age <<":"<<*p3.m_Age<<endl; }
5.5 关系运算符重载
-
作用: 重载关系运算符,可以让两个自定义类型对象进行对比操作
class Person { public: Person(string name, int age) { m_Name = name; m_Age = age; } bool operator==(const Person &p) { if(m_Name == p.m_Name && m_Age == p.m_Age) { return true; } else { return false; } } string m_Name; int m_Age; }; void test() { Person p1("张三", 20); Person p2("张三", 20); Person p3("李四", 20); if(p1 == p2) { cout <<"p1 == p2"<<endl; } if(p1 == p3) { cout <<"p1 == p3"<<endl; } }
5.6 函数调用运算符重载
-
函数调用运算符() 也可以重载
-
由于重载后使用的方式非常像函数的调用,因此称为仿函数
-
仿函数没有固定写法,非常灵活
-
这是创建一个可以传递任意数目参数的运算符函数。
//输出类 class MyPrint { public: void operator()(string data) { cout <<data <<endl; } }; //加法类 class MyAdd { public: int operator()(int num1, int num2) { return num1 + num2; } }; void test() { MyPrint p; p("hello"); //使用非常像函数调用 } void test1() { MyAdd a; cout <<a(10, 20) <<endl; //匿名函数对象 cout <<MyAdd()(20, 30) <<endl; }
6 继承
- 继承是面向对象的三大特性之一
- 继承允许我们依据另一个类来定义一个类,这使得创建和维护一个应用程序变得更容易。这样做,也达到了重用代码功能和提高执行效率的效果。
- 已有的类称为基类,新建的类称为派生类
6.1 基本语法
-
class derived-class: access-specifier base-class
-
访问修饰符 access-specifier 是 public、protected 或 private 其中的一个,base-class 是之前定义过的某个类的名称。如果未使用访问修饰符 access-specifier,则默认为 private。
-
继承的方式一共有三种
- 公共继承
- 保护继承
- 私有继承
6.2 继承中的对象模型
- 从父类继承过来的成员,哪些属于子类对象中?
- 父类中所有非静态成员属性都会被子类继承,除了:
- 基类的构造函数、析构函数、拷贝构造
- 基类的重载运算符
- 基类的友元函数
6.3 继承中构造和析构顺序
-
子类继承父类后,当创建子类对象,也会调用父类的构造函数
-
先构造父类,后构造子类,析构顺序与构造顺序相反
class Base { public: Base() { cout <<"构造Base" <<endl; } ~Base() { cout <<"析构Base" <<endl; } }; class Son :public Base { public: Son() { cout <<"构造Son" << endl; } ~Son() { cout <<"析构Son" << endl; } }; int main() { Son s; return 0; } /**********************************/ 构造Base 构造Son 析构Son 析构Base
6.4 继承同名成员处理方式
-
访问子类同名成员,直接访问
-
访问父类同名成员,需要加作用域
-
如果子类中出现和父类中同名的成员函数,子类的同名成员函数会隐藏掉父类中所有的同名成员函数,包括重载。
class Base { public: Base() { m_A = 10; } void func() { cout <<"Base函数" <<endl; } void func(int a) { cout <<a <<endl; } int m_A; }; class Son : public Base { public: Son() { m_A = 20; } void func() { cout <<"Son函数" <<endl; } int m_A; }; void test() { Son s; cout << s.m_A <<endl; cout << s.Base::m_A <<endl; //访问父类同名成员属性 //调用成员函数 s.func(); s.Base::func(); s.Base::func(12); //隐藏掉父类中的同名重载函数 }
6.5 继承中同名静态成员处理方法
- 继承中同名的静态成员在子类上如何进行访问
- 静态成员和非静态成员出现同名,处理方法一致
- 访问子类同名成员,直接访问
- 访问父类同名成员,加作用域
6.6 多继承语法
- C++允许一个类继承多个类
- 语法:
clas 子类:继承方式 父类1,继承方式 父类2
- 多继承可能会引发父类中友同名成员出现,需要加作用域区分
- C++开发中不建议使用多继承
- 语法:
6.7 菱形继承
-
概念
- 两派生类继承同一个基类
- 又有某个类同时继承两派生类
- 称为菱形继承或钻石继承
-
缺点
-
导致重复继承,浪费内存且出现二义性
-
可利用虚继承解决该问题
-
继承之前加上关键字virtual,变为虚继承
-
类中会多出vbptr(指向虚基类表)指针
class a1 { public: int m_A; }; class a2:virtual public a1 { public: int m_B; }; class a3:virtual public a1 { public: int m_C; }; class a4:public a2, public a3 { public: int m_D; }; int main() { a4 a; cout <<sizeof(a2) <<endl; cout <<sizeof(a3) <<endl; cout <<sizeof(a) <<endl; return 0; }
-
7 多态
7.1 多态的基本概念
-
多态是C++面向对象三大特性之一
-
多态分为两类
- 静态多态: 函数重载和运算符重载属于静态多态,复用函数名
- 动态多态: 派生类和虚函数实现运行时多态
-
静态多态和动态多态的区别
- 静态多态的函数地址早绑定,编译阶段确定函数地址
- 动态多态的函数地址晚绑定,运行时确定函数地址
-
动态多态条件
- 有继承关系
- 子类重写父类的虚函数
-
动态多态使用
-
父类的指针或引用,执行子类对象
void test() { Base *base = new Son; //Base 父类 Son 子类 base->func(); }
-
-
重写(覆盖)
- 子类重新定义父类中有相同名称和参数的虚函数(virtual)
- 函数返回值类型、函数名、参数列表完全相同
- 被重写的函数不能是static的。必须是virtual的
- 重写函数的访问修饰符可以不同。尽管父类的virtual方法是private的,派生类中重写改写为public,protected也是可以的。
-
虚函数
- 在派生类中重新定义基类中定义的虚函数时,会告诉编译器不要静态链接到该函数。
- 在程序中任意点可以根据所调用的对象类型来选择调用的函数,这种操作被称为动态链接,或后期绑定。
class Animal
{
public:
virtual void speak() //虚函数
{
cout <<"说话" <<endl;
}
};
class Cat:public Animal
{
public:
void speak() //重写虚函数
{
cout <<"猫说话"<<endl;
}
};
class Dog:public Animal
{
public:
void speak()
{
cout <<"狗说话"<<endl;
}
};
//说话
//地址早绑定,在编译阶段确定函数地址
//如果想执行让猫说话,就不能让函数地址早绑定,需要在运行阶段绑定
void doSpeak(Animal &animal) //父类引用可以直接指向子对象
{
animal.speak();
cout <<& animal <<endl;
}
void test()
{
//猫说话
Cat cat;
doSpeak(cat);
//狗说话
Dog dog;
doSpeak(dog);
}
7.2 多态案例-计算器类
-
案例描述
- 分别利用普通写法和多态技术,设计实现两个操作数进行运算的计算器类
-
多态的优点
- 代码组织结构清晰
- 可读性强
- 利于前后期的扩展及维护
-
程序开发中
- 提出开闭原则
- 开闭原则:对扩展进行开放,对修改进行关闭
-
示例
//利于多态实现计算器 //实现计算器抽象类 class AbstractCalculator { public: virtual int getResult() { return 0; } int m_Num1; int m_Num2; }; //加法类 class AddCalculator: public AbstractCalculator { int getResult() { return m_Num1 + m_Num2; } }; //减法类 class SubCalculator: public AbstractCalculator { int getResult() { return m_Num1 - m_Num2; } }; void test02() { AbstractCalculator * abc = new AddCalculator; abc->m_Num1 = 10; abc->m_Num2 = 20; cout << abc->getResult() <<endl; }
7.3 纯虚函数和抽象类
-
在多态中,通常父类中虚函数的实现是毫无一样的,主要都是调用子类重写的内容,因此可以将虚函数改为纯虚函数
-
语法:
virtual 返回值类型 函数名 (参数列表) = 0;
-
当类中有了纯虚函数,这个类称为抽象类
-
抽象类特点
- 无法实例化对象
- 子类必须重写类冲的纯虚函数,否则也属于抽象类
7.4 虚析构和纯虚析构
-
多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码
-
解决方法:将父类中的析构函数改为虚析构或者纯虚析构
-
虚析构和纯虚析构共性
- 可以解决父类制作释放子类对象
- 都需要具体的函数实现
-
虚析构和纯虚析构的区别
- 如果是纯虚析构,该类属于抽象类,无法实例化对象
-
虚析构语法
virtual ~类名(){}
-
纯虚析构语法
virtual ~类名() = 0;
- 纯虚析构函数体:
类名::~类名(){}
-
示例
class Animal { public: Animal() { cout <<"Animal构造" <<endl; } virtual void speak() = 0; virtual ~Animal() = 0; //纯虚析构 // virtual ~Animal() //虚析构 // { // cout <<"Animal析构" <<endl; // } }; //纯虚析构函数体 Animal::~Animal() { cout << "Animal析构" <<endl; } class Cat: public Animal { public: Cat(string name) { cout <<"Cat构造" <<endl; m_name = new string(name); } virtual void speak() { cout << *m_name<<"说话" << endl; } string *m_name; ~Cat() { delete m_name; cout << "Cat析构" <<endl; } }; void test() { Animal * animal = new Cat("张三"); animal->speak(); delete animal; animal = nullptr; }
16、文件操作
-
程序运行时产生的数据都属于临时数据,程序一旦运行结束都会被释放
-
通过文件可以将数据持久化
-
C++中对文件操作需要包含头文件<fstream>
-
文件类型
- 文本文件:文件以文本的ASCII码形式存储在计算机中
- 二进制文件:文件以文本的二进制形式存储在计算机中
-
操作文件的三大类
- ofstream:写操作
- ifstream:读操作
- fstream:读写操作
1 文本文件
1.1 写文件
-
步骤
#include <fstream> //包含头文件 ofstream ofs; //创建流对象 ofs.open("文件路径",打开方式); ofs << "写入数据"; ofs.close(); // 关闭文件
1.2 读文件
-
步骤
#include <fstream> //包含头文件 ifstream ifs; //创建流对象 ifs.open("文件路径", 打开方式); if(!ifs.is_open()) //判断文件是否打开成功 { cout <<"打开失败"<<endl; return; } //读数据 //第一种读取方式 char str[100] = {0}; while(ifs >> str) { cout <<str ; } //第二种方法 while( ifs.getline(str, sizeof(str))) { cout <<str; } //第三种方法 string buf; while(getline(ifs, buf)) { cout <<buf; } //第四次方法 char c; while( (c = ifs.get()) != EOF) { cout << c; }
2 二进制文件
- 以二进制方式对文件进行读写操作
- 打开方式要指定为
ios::binary
2.1 写文件
- 二进制方式写文件主要利用流对象调用成员函数
write
- 函数原型:
ostream write (const char * buffer , int len );
- 参数解释:字符指针buffer指向内存最后一段存储空间,len是读写的字节数
class Person
{
public:
char m_Name[100]; //这里写字符串最好不用c++的string,因为会出现一些问题
int m_Age;
};
void test()
{
ofstream ofs;
ofs.open("txt1", ios::out | ios::binary);
Person p = {"张三", 18};
ofs.write((const char *)&p, sizeof(Person));
ofs.close();
}
2.2 读文件
- 二进制方式读文件主要利用流对象调用成员函数read
- 函数原型:
istream & read (char *buffer, int len);
- 参数解释:字符指针buffer指向内存中一段存储空间,len是读写的字节书
class Person
{
public:
char m_Name[100];
int m_Age;
};
void test()
{
ifstream ifs;
ifs.open("txt1", ios::in | ios::binary);
if(!ifs.is_open())
{
cout <<"打开失败" <<endl;
return ;
}
Person p;
ifs.read((char *)&p, sizeof(Person));
cout <<p.m_Name <<":"<<p.m_Age <<endl;
ifs.clear();
}!