C++ 核心编程
学习网址:http://c.biancheng.net/cplus/
学习笔记:https://blog.csdn.net/ClaireSy/article/details/108423047
- 内存的分区模型
- new 操作符
- 引用
- 函数提高
- 类与对象
- 对象的初始化与清理
- 深拷贝与浅拷贝
- 初始化列表
- 静态成员函数
- C++ 对象模型与 this 指针
- 友元
- 基本数据类型大小
- 运算符重载
- 继承
- 多态
- 文件操作
1.内存的分区模型
- 代码区 存放函数的二进制代码,由操作系统进行管理
- 全局区
- 栈区
- 堆区
分区的意义:不同的区域拥有不同的生命周期,有更大的灵活编程
程序运行前
代码区
存放cup 执行的二进制机器指向
代码区是共享的,共享的目的是对于频执行的程序,只需要在内存中存一份即可
代码区是只读的,防止程序意外修改
全局区
全局变量和静态变量
常量区(全局常量、const 修饰的全局变量)
程序运行后
栈区
编译器自动的分配与释放,存放函数形参,局部变量等
(注意:不要返回局部变量的地址,第一次可以打印正确的局部变量返回,第二次就丢失)
堆区
由程序员分配释放,如果程序员不释放,程序结束后由操作系统回收
new 可以在堆区开辟内存
2.new 操作符
操作: new 数据类型
释放: delete 变量 ,释放数组 delete[] 数组名
3.引用
语法: 数据类型 &别名 = 原名
int a = 10; int &b = a; b = 20; cout << a << endl;
引用必须初始化,不可更改
使用引用作为方法参数,修改实参
void swap_3(int &a, int &b) { int temp = a; a = b; b = temp; }
不要返回局部变量的引用(第一次输出正常,第二次结果是错误的,编译器只保留一次)
静态变量存储在全局区,全局区上的数据在程序结束后释放
函数的调用可以作为左值
#include<iostream> #include<string.h> using namespace std; int& test() { static int a = 10; return a; } int main() { int& ref = test(); cout << "ref=" << ref << endl; cout << "ref=" << ref << endl; test() = 100; cout << "ref=" << ref << endl; cout << "ref=" << ref << endl; system("pause"); return 0; }
引用的本质:引用的本质在c++内部是一个指针常量
int a = 10; // 等价于 int * const ref = &a; int& ref = a; //*ref = 20; ref = 20; cout << "a=" << a << endl; cout << "ref=" << ref << endl;
常量引用:用来修饰形参,防止误操作(在方法形参中使用const 修饰引用,则引用无法被修改)
int a = 10; //等价于 int temp = 10; const int& ref = temp; const int& ref = 10;
4.函数提高
默认参数
函数的默认参数,如果有参数则使用传递的参数,没有则使用默认参数
如果多个参数第一个出现默认参数,则之后的参数都需要有默认参数
void add(int a= 100,int b= 200,int c= 300) { int sum = a + b + c; cout << sum << endl; } int main() { int a = 10; int b = 20; int c = 30; add(10); system("pause"); return 0; }
注意:函数的声明出现默认参数,则实现无法使用默认参数
占位参数
函数的形参列表可以有占位参数,用来做占位,调用函数时必须填补改参数
void add(int a,int) { int sum = a; cout << sum << endl; }
占位参数可以有默认参数,则不必传递该参数
void add(int a,int = 20) { int sum = a; cout << sum << endl; }
函数重载
函数名相同,提高复用性
- 同一个作用域
- 函数名相同
- 函数的参数类型不同、个数不同、顺序不同
注意
引用可以作为重载的条件(传递变量与常量不同)
void test(int &a) { cout << "a" << endl; } void test(const int& a) { cout << "const a" << endl; }
函数重载碰到默认参数(传递1个参数出现二义性,传递2个参数正常)
void test(int a,int=10) { cout << "int a int" << endl; } void test(int a) { cout << "int a" << endl; }
5.类与对象
C++ 面向对象三大特性:封装、继承、多态
访问权限
public protected private
共有权限:类内可以访问,类外可以访问
保护权限:类内可以访问,类外不可访问,子类可以访问父类的保护内容
私有权限:类内可以访问,类外不可访问,子类不可访问
struct 与 class 的区别
struct 默认的权限是共有, class 默认的权限是私有
struct C1 { int age; void fun() { cout << "struct" << endl; } }; class C2 { int age; void fun() { cout << "struct" << endl; } }; int main() { C1 c; c.fun(); C2 c2; //默认私有权限 return 0; }
6.对象的初始化与清理
构造函数与析构函数,这两个函数会被编译器自动调用,完成初始化与清理工作。
构造函数
类型 (){}
- 构造函数无返回值
- 函数名与类目相同
- 可以有参数,可以重载
- 创建对象,会自动调用构造
析构函数
~类名(){}
- 不能有参数,不能发生重载
- 析构函数在对象被销毁前调用
class Person { public: Person() { cout << "构造函数" << endl; } ~Person(){ cout << "析构函数" << endl; } }; int main() { Person p; return 0; }
注意:构造函数可以接收对象,实现拷贝构造
创建对象的三种方式
//1.隐式调用 //Person p; //该调用被识别为函数声明 //Person p(); //2.显式调用 //Person p = Person(); //以下是创建匿名对象,执行完即释放 //Person(); //3.隐式转换法 等价与 Person p = Person(10); Person p = 10;
默认情况下,C++ 编译器至少添加3个函数
- 默认构造函数(无参)
- 默认析构函数
- 默认拷贝构造函数,对属性进行值拷贝
规则如下
如果用户定义了有参构造,则c++ 不提供无参构造,但是会提供拷贝构造
如果用户定义了拷贝构造函数,c++ 不提供其他构造函数
7.深拷贝与浅拷贝
编译器提供的拷贝都是浅拷贝 ,会导致内存重复释放
注意:如果属性是堆区开辟的内存,那么一定要实现深拷贝
class Person { public: int age; int* hight; Person() { cout << "构造函数" << endl; } Person(int n) { cout << "构造函数" << endl; } Person(const Person &p) { age = p.age; hight = new int(*p.hight); } ~Person(){ if (hight != NULL) { delete hight; hight = NULL; } cout << "析构函数" << endl; } };
如果对象属性包含在堆创建的属性,那么析构进行回收,如果未实现深拷贝则会出现问题
8.初始化列表
class Person { public: int m_a; int m_b; int m_c; Person(int a,int b,int c):m_a(a),m_b(b),m_c(c){ } Person(const Person& p) { m_a = p.m_a; m_b = p.m_b; m_c = p.m_c; } ~Person() { } }; int main() { Person p(10, 20, 30); return 0; }
初始化列表可以在构造函数外实现参数的初始化
类对象作为成员: 当 A 类的对象作为 B 类的成员时,创建B的时候首先创建 A ,析构的顺序相反(注意:如果传参使用参数列表,则A 创建一个对象,如果用赋值,则创建了2个对象 ,包括一个无参)
9.静态成员函数
静态成员变量
- 所有对象共享一份数据
- 在编译阶段分配内存
- 类内声明,类外访问
静态成员函数
- 所有对象共享一个函数
- 静态成员函数只能访问静态成员变量
class Person { public: string age; static int high; static void print() { high = 200; //cout << age << endl; cout << high << endl; } }; //静态成员变量类外初始化 int Person::high = 10; int main() { //方式一、对象访问静态成员函数 Person p; p.print(); //方式二、通过类名访问 Person::print(); return 0; }
10.c++ 对象模型与 this 指针
成员变量与成员函数分开存储
空对象占用内存空间为 1
如果非空对象,则对象的大小与成员属性(非静态)相关
class Person { public: int age; static string name; void say() {} }; int main() { Person p; cout << sizeof(p) << endl; return 0; }
如果是空对象则为1 ,否则为普通成员函数的内存大小
注意:单独的double 占用8个字节,与其他字节组合,则占用8的倍数个字节
this 指针概念
- 指向被调用成员函数所属的对象
- 空指针可以访问成员函数,如果成员函数用到this ,才会报错
const 修饰的成员函数
- 成员函数加 const 后称为常含数
- 常含数不可修改属性
- 成员属性声明时加 mutable 之后,在常含数中依然可以修改
常对象
- 声明对象前加const 称为常对象
- 常对象只能调用常含数
this 指针本质是指针常量: Person * const this; 指针不可修改,值可修改。 常含数加const 本质是: const Person * const this ,指针与值都不可修改
成员变量加特殊后可以修改 : mutable
class Person { public: int m_A; mutable int m_B; void say() const{ //m_A = 10; 不可修改 m_B = 20; } }; int main() { const Person p; //p.m_A = 20; 不可修改 p.m_B = 20; p.say(); return 0; }
11.友元
友元:目的是让全局函数、类、成员函数可以访问当前类的私有属性,关键词:friend
全局函数作有缘
类作友元
成员函数作友元
12.基本数据类型的大小
基本数据类型的大小(window系统)
32位系统
64位系统
注意:Linux 系统下 long 类型是 8 字节
VC++ 关于 sizeof(string) 为何28(x86)位 与 40 (x64) 个字节
名称 | X86 (字节数) | X64(字节数) |
Allocator | 4 | 8 |
原始字符传 Data 位置 | 15+1 最多包含15个字符加一个结束符‘\0’ | 15+1 最多包含15个字符加一个结束符‘\0’ |
字符长度 Size | 4 | 8 |
当前容量 Capacity | 4 | 8 |
总计 | 28 | 40 |
#include<iostream> using namespace std; /* 1.空结构体/类的对象大小位 1B 2.类有成员属性(单独) x64 x86 char 1b 1b short 2b 2b int 4b 4b float 4b 4b double 8b 8b long long 8b 8b int * 8b 4b string 40b 28b 3.组合类型 x64 x86 char + char = 2 2 char + short = 4 4 char + int = 8 8 char + float = 8 8 char + int * = 16 8 char + string = 48 32 int + string = 48 32 int + double = 16 16 string + string = 80 56 string + double = 48 40 float + int * = 16 8 */ class Person { float age; int * name; }; int main() { Person p; cout << sizeof(p) << endl; return 0; }
13.运算符重载
运算符重载:可以对对象之间的运算符操作
+ 号运算符重载,通用函数 operator
#include<iostream> using namespace std; class Person { public: int m_A; int m_B; //成员函数重载 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; } }; //全局函数重载 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; } int main() { Person p1; p1.m_A = 10; p1.m_B = 10; Person p2; p2.m_A = 20; p2.m_B = 20; Person p3 = p1 - p2; cout << p3.m_A << endl; cout << p3.m_B << endl; return 0; }
<< 重载,输出类
#include<iostream> #include<string.h> using namespace std; class Person { public: int m_A; int m_B; }; ostream& operator<<(ostream &cout,Person &p) { cout << "m_A=" << p.m_A << ",m_B=" << p.m_B; return cout; } int main() { Person p1; p1.m_A = 10; p1.m_B = 10; cout << p1<<endl; return 0; }
递减运算符重载
#include<iostream> #include<string.h> using namespace std; //实现自定义Int 类型,并实现 -- 重载 class MyInt { friend ostream& operator<<(ostream& cout, MyInt m); private: int number; public: MyInt(){ number = 0; } MyInt& operator--() { number--; return *this; } MyInt operator--(int) { MyInt temp = *this; number--; return temp; } }; ostream& operator<<(ostream& cout, MyInt m) { cout << m.number; return cout; } int main() { MyInt m; //cout << --(--m) <<endl ; cout << (m--)-- << endl; cout << m << endl; return 0; }
注意:
前置递减和后置的重载通过占位符
前置递减返回的是引用,可以连续操作,后置递减返回临时值,不能连续的操作
赋值运算符重载
编译器提供的赋值运算符实现的是浅拷贝,需要实现赋值运算符深拷贝运算符重载。
关系运算符重载
可以实现对象之间的等于与不等于比较
函数调用运算符重载
对象内部实现() 重载,可以通过对象调用。称为仿函数
匿名函数对象,创建匿名对象调用了函数运算符重载
14.继承
语法
class 类 : public 父类{
}
子类也叫做派生类,父类也叫做基本
继承方式
- 公共继承 与父类一致
- 保护继承 父类共有变为保护
- 私有继承 父类公共与保护变为私有
继承中的对象模型
父类中所有的属性都会被继承,私有成员属性是被隐藏的,无法访问
//利用开发人员命令工具查看对象模型
cl /d1 reportSingleClassLayout类名 文件名
C++ 允许多继承
多继承通过逗号分割
不建议使用多继承,多继承可能会出现同名的变量、函数,需要加作用域
菱形继承/钻石继承
问题:当多个父类中包含重复的属性时,多继承浪费资源且毫无意义
解决:虚继承 virtual ()
父类继承公共类的时候使用虚继承,(vbptr 虚基类指针)
vbptr 指向了 vbtable (虚基类表), 通过偏移量指向相同的数据
15.多态
静态多态: 函数重载,运算符重载属于静态多态
动态多态: 派生类与虚函数实现运行时多态
正常情况下,父类指向子类,指向的是父类的函数,因为是地址早绑定。要使用传入对象的方法,需要将父类的方法标记为 virtual ,即地址晚绑定
多态注意:子类要重写父类的虚函数(子类virtual 可以忽略)
原理:
父类指定virtual 函数后,内部多一个 vfpter 指针,指向虚函数表, vftable (该表存储虚函数地址)。
当子类重写虚函数后,继承 vfptr ,同时虚函数表存储的函数地址修改为子类的虚函数地址
纯虚函数
virtual 返回值类型 函数名(参数列表) = 0;
当类中有纯虚函数,该类为抽象类,特点是无法实例化,子类必须重写纯虚函数
虚析构与纯虚析构
解决问题:当子类对象中内存开辟到堆区,多态下父类指针无法释放子类对象,会造成内存泄漏
将子类的析构函数用 virtual 修饰,改为虚析构,那么释放父类指针也会调用子类的析构
另一种解决办法:使用纯虚析构 ,将父类的析构改为纯虚析构,需要类外实现
#include<iostream> using std::cout; using std::endl; using std::cin; class Base { public: Base() { cout << "base 构造" << endl; } virtual void speak() { cout<<"Base speak"<<endl; } virtual ~Base() { cout << "base 析构" << endl; } }; class Son : public Base { public: Son() { cout << "Son 构造" << endl; } void speak() { cout << "Son speak" << endl; } ~Son() { cout << "Son 析构" << endl; } }; int main() { Base *base = new Son; base->speak(); delete base; return 0; }
16.文件操作
C++ 中文件操作需要包含头文件 <fstream>
文件类型分为两中
- 文本文件 以ASCII 码形式存储
- 二进制文件 文件以文本的二进制形式存储,一般无法识别
操作文件的三大类
- ofstream 写操作
- ifstream 读操作
- fstream 读写操作
打开方式
- ios::in 为读文件而打开文件
- ios::out 为写文件而打开文件
- ios::ate 初始位置:文件尾
- ios::app 追加方式写文件
- ios::trunc 如果文件存在先删除,再创建
- ios::binary 二进制方式
写文件案例
#include<iostream> #include<fstream> using namespace std; int main() { ofstream ofs; ofs.open("text.txt",ios::out); ofs << "Hello,file "; ofs.flush(); ofs.close(); return 0; }
读文件 ifstrean
#include<iostream> #include<fstream> #include<string> using namespace std; int main() { ifstream ifs; ifs.open("text.txt",ios::in); if (!ifs.is_open()) { cout << "文件打开失败" << endl; } //第一种读取 /* char buff[1024] = {0}; while (ifs >> buff) { cout << buff << endl; } */ //第二种方式 /* char buff[1024] = {0}; while (ifs.getline(buff, sizeof(buff))) { cout << buff << endl; } */ //第三种 /* string buff; while (getline(ifs, buff)) { cout << buff << endl; } */ //第四种 ,EOF 代表文件尾部 char ch; while ((ch = ifs.get())!=EOF) { cout << ch; } ifs.close(); return 0; }