备注:本笔记作查漏补缺用,只记重点,只记干货,多的一句也不啰嗦!
数据类型 | 占用空间 | 取值范围 |
---|---|---|
short(短整型) | 2字节 | -2^15 ~ 2^15 - 1 |
int(整型) | 4字节 | -2^31 ~ 2^31 - 1 |
long(长整型) | Windows为4字节,Linux为4字节(32位OS)或8字节(64位OS) | -2^31 ~ 2^31 - 1 |
long long(长长整型) | 8字节 | -2^63 ~ 2^63 - 1 |
备注:使用sizeof()可以获取某一变量或数据类型所占的字节数
数据类型 | 占用空间 | 有效数字范围 |
---|---|---|
float | 4字节 | 7位有效数字 |
double | 8字节 | 15~16位有效数字 |
备注:
C/C++中字符型变量只占用1个字节
字符型变量并不是把字符本身放到内存中存储,而是将对应的ASCII编码放入存储单元(在0~256范围内int和char可以相互转换)
C风格字符串
char 变量名[] = "字符串内容";
C++风格字符串
string 变量名 = "字符串内容";
bool类型数据只有两个值:
bool类型占1个字节大小
函数的声明方式
type 函数名 (参数列表);
函数的定义方式
type 函数名 (参数列表){
…
函数体
…
return 返回值;
}
备注:函数可被多次声明,但只能被定义一次
作用:让代码结构更清晰
函数分文件编写一般有4个步骤:
//指针定义的语法:数据类型 *指针变量名;
int a = 10;
int *p = &a;
cout << "*p = " << *p << '\n' << "p = " << p << endl;
//=> *p = 10
// p = 00AFF77C
备注:指针使用时前面不加 * 代表的是一个内存地址,加上 * 则代表的是这个内存地址中存储的数据。
指针变量在32位操作系统中所占的内存空间为4个字节,在64位操作系统中占8个字节。
const修饰指针——常量指针
int a = 20;
int b = 10;
const int *p = &a;
*p = 10;//错误,常量指针指向可以改,但指向的值不能改
p = b;//正确
const修饰变量——指针常量
int a = 20;
int b = 10;
int * const p = &a;
*p = 10;//正确,指针常量指向不能改,但指向的值可以改
p = b;//错误
const既修饰指针又修饰变量
int a = 20;
int b = 10;
const int * const p = &a;
*p = 10;//错误,const既修饰指针又修饰变量时指向不能改,指向的值也不能改
p = b;//错误
C++程序在执行时,将内存大方向划分为4个区域:
代码区:存放函数体的二进制代码,由操作系统进行管理
存放CPU执行的机器指令
代码区是共享的,共享的目的是对于频繁被执行的程序,只需要在内存中有一份代码即可
代码区是只读的,使其只读的原因是防止程序意外地修改了它的指令
全局区:存放全局变量和静态变量以及常量
全局变量和静态变量存在于此
全局区还包含了常量区,字符串常量和其他常量也存放在此
该区的数据在程序结束后由操作系统释放
栈区:由编译器自动分配释放,存放函数的参数值,局部变量等
由编译器自动分配释放,存放函数的参数值,局部变量等
注意事项:不要返回局部变量的地址,栈区开辟的数据在局部代码块执行完后由编译器自动释放。第一次使用局部变量地址时编译器会保留局部变量数值,第二次就被抹去了。
堆区:由程序员分配和释放,若程序员不释放,程序结束时由操作系统进行回收
由程序员分配释放,若程序员不释放,程序结束时由操作系统释放
在C++中主要使用 new 关键字在堆区开辟内存,利用 new 创建的数据,会返回该数据对应类型的指针
注意事项:堆区开辟的数据,由程序员手动开辟手动释放,释放时使用关键字 delete
内存四区的意义:
不同区域存放的数据,赋予不同的生命周期,使编程更灵活
备注:代码区、全局区在程序执行前构建,栈区和堆区在程序执行后构建
C++面向对象的三大特性为:封装、继承和多态
C++认为万事万物都是对象,对象上由其属性和行为
分类
调用方式
class Person{
private:
int age;
public:
Person(){}//无参构造
Person(int a){//有参构造
age = a;
}
Person(const Person &p){//拷贝构造
age = p.age;
}
};
括号法
void test01(){
Person p;//无参构造函数的括号法调用
Person p2(10);//有参构造函数的括号法调用
Person p3(p2);//拷贝构造函数的括号法调用,将p2身上的属性拷贝给p3
}
注意:调用无参构造函数时切记不要写括号,否则编译器会认为那是一个函数声明,而不是在创建对象
显示法
void test01(){
Person p;//无参构造函数的显示法调用
Person p2 = Person(10);//有参构造函数的显示法调用
Person p3 = Person(p2);//拷贝构造函数的显示法调用
}
注意:
隐式转换法
void test01(){
Person p2 = 10;//等价于Person p2 = Person(10);
Person p3 = p2;//等价于Person p3 = Person(p2);
}
拷贝构造函数的调用时机
C++中拷贝构造函数调用的时机通常有三种情况:
C++中构造函数的生成规则
默认情况下,C++编译器至少给一个类生成3个函数
生成规则:
深拷贝与浅拷贝
浅拷贝:简单的赋值操作
深拷贝:在堆区重新申请空间,进行拷贝操作
注意:所有由编译器自动生成的拷贝构造函数,都是浅拷贝;想要深拷贝就必须自己写能实现深拷贝的拷贝构造函数,尤其是牵扯到指针变量的拷贝,都属于深拷贝
构造函数的偷懒写法——初始化列表
//Person类的构造函数
Person(int age, double height){
this->age = age;
this->height = height;
}
//初始化列表写法
Person(int age, double height): this->age(age), this->height(height)
{}//注意:函数体虽为空,但不能省略
注意:初始化列表方式支持使用隐式转换法构造对象成员
在程序里,有些私有属性也想让类外的一些特殊函数或者类访问,就需要用到友元技术。友元的目的就是让一个函数或者类访问另一个类中私有成员,关键字为friend
全局函数做友元
class House{
//在类中声明友元函数
friend void goodFriend(House &house);
public:
Building(){
this->livingRoom = "客厅";
this->bedRoom = "卧室";
}
private:
string livingRoom;
string bedRoom;
}
void goodFriend(House &house){
//在House类的友元函数中查看House对象的所有成员
cout << "全局函数goodFriend正在访问 " << house.livingRoom;
cout << "全局函数goodFriend正在访问 " << house.bedRoom;
}
友元类
class House; //House类的声明,避免编译器报错
class GoodFriend{
//定义goodFriend友元类
public:
House *house;
GoodFriend();
void visit();//参观函数
};
GoodFriend::GoodFriend(){
house = new House;
}
void GoodFriend::visit(){//访问House对象中的成员
cout << "GoodFriend类正在访问 " << house->livingRoom << endl;
cout << "GoodFriend类正在访问 " << house->bedRoom << endl;
}
class House(){
friend class GoodFriend;//声明GoodFriend类是House类的友元类
public:
string livingRoom;
House();
private:
string bedRoom;
};
House::House(){
this->livingRoom = "客厅";
this->bedRoom = "卧室";
}
成员函数做友元
class House;
class GoodFriend{
public:
House *house;
GoodFriend(){
house = new House;
}
void visit(){//友元函数
cout << "visit函数正在访问 " << house->livingRoom << endl;
cout << "visit函数正在访问 " << house->bedRoom << endl;
}
void nvisit(){//与友元成员函数区别的普通成员函数
cout << "nvisit函数正在访问 " << house->livingRoom << endl;
cout << "nvisit函数正在访问 " << house->bedRoom << endl;//报错,不是友元,无法访问House的私有成员
}
};
class House{
friend void GoodFriend::visit();//声明友元成员函数
public:
string livingRoom;
House(){
livingRoom = "客厅";
bedRoom = "卧室";
}
private:
string bedRoom;
};
运算符重载就是对已有的运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型
Complex复数类,支持加减运算
#include
using namespace std;
class Complex
{
private:
double real;//实部
double virt;//虚部
public:
Complex();//无参构造函数
Complex(double real, double virt);//含参构造函数
Complex operator+(Complex &other);//加号重载
Complex operator-(Complex& other);//减号重载
void print();//输出函数
};
//类中方法的实现
Complex::Complex(){
real = 0;
virt = 0;
}
Complex::Complex(double real, double virt) {
this->real = real;
this->virt = virt;
}
Complex Complex::operator+(Complex& other) {//复数加法的重载
Complex temp(this->real + other.real, this->virt + other.virt);
return temp;
}
Complex Complex::operator-(Complex& other) {//复数减法的重载
Complex temp(this->real - other.real, this->virt - other.virt);
return temp;
}
void Complex::print()
{
if (virt >= 0)
{
cout << real << " + " << virt << "i" << endl;
}
else
{
cout << real << " - " << -virt << "i" << endl;
}
}
注意:
Java中的toString方法在C++中用重载左移运算符 << 来实现,且只能用全局函数重载左移运算符:(以复数类为例)
//传ostream&是为了返回cout对象,返回cout对象是为了实现链式输出
ostream& operator<<(ostream &cout, Complex &c){
if (virt >= 0)
{
cout << c.real << "+" << c.virt << "i" << endl;
}
else
{
cout << c.real << "-" << -c.virt << "i" << endl;
}
return cout;
}
//为了使全局函数能够访问到Complex类的私有成员,我们可以将这个全局函数设为Complex类的友元
class Complex{
friend ostream& operator<<(ostream &cout, Complex &c);
……
};
递增运算符的重载
class MyInteger {
//声明友元函数
friend ostream& operator<<(ostream& cout, MyInteger m);
private:
int data;
public:
MyInteger(int d) {
data = d;
}
MyInteger& operator++() {//前置++运算符重载
data++;
return *this;//返回自身是为了能够实现连续操作
}
MyInteger operator++(int) {//后置++运算符重载
//因为后置++运算的运算逻辑是先用后加,所以
//1. 记录当时的结果
MyInteger temp = *this;
//2. 递增
this->data++;
//3. 返回递增前的结果
return temp;
//不返回自身的引用是为了防止当前对象被编译器当作局部变量在函数执行完后直接释放
}
};
ostream& operator<<(ostream& cout, MyInteger m) {//<<运算符号重载
cout << m.data;
return cout;
}
赋值运算符重载
编译器默认的赋值运算符是浅拷贝,类的属性中有用到深拷贝时就需要对赋值运算符进行重载了
class Person {
friend ostream& operator<<(ostream& cout, Person& p);
private:
int* age;//创建一个堆区的属性
public:
Person(int age) {
this->age = new int(age);
}
~Person() {
if (age != nullptr)
{
delete(age);//释放堆区所占内存
age = NULL;//防止非法访问,需要彻底断绝age与内存的联系
}
}
Person& operator=(Person& p) {
//先判断自身是否已经赋初值
if (age == nullptr) {
delete age;//若已经赋初值则释放掉
}
age = new int(*p.age);
return *this;//返回自身是为了实现连续赋值
}
};
ostream& operator<<(ostream& cout, Person& p) {//重载<<运算符
cout << *p.age << endl;
return cout;
}
关系运算符重载
//例:实现Complex复数类中 == 和 != 号的重载
bool operator==(Complex& other);//判断两个复数是否相等
bool operator!=(Complex& other);//判断两个复数是否不相等
//具体实现
bool Complex::operator==(Complex& other)//判断两个复数是否相等
{
if (real == other.real && virt == other.virt) {
return true;
}
else {
return false;
}
}
bool Complex::operator!=(Complex& other)//判断两个复数是否不相等
{
if (real == other.real && virt == other.virt) {
return false;
}
else {
return true;
}
}
仿函数——重载函数调用运算符()
仿函数在STL部分用处很大,因为该运算符实现后与真正的函数调用别无二致,因此得名仿函数。仿函数没有固定的写法,非常灵活
class MyPrint {//实例:打印输出类
public:
void operator()(string text) {//重载函数调用运算符
cout << text << endl;
}
};
void test() {
MyPrint m;
m("hello, world!");//函数调用运算符的用法
}
基本概念
多态是C++面向对象三大特性之一
多态分为两类:
静态多态和动态多态的区别:
多态的实现
实现静态多态只需要重载函数或运算符即可,使其在代码编译时就能准确找到需要调用哪些方法或者运算符;而实现动态多态则需要使用到 virtual 关键字来构建虚函数和抽象类,通过在子类中重写对应的函数来实现动态多态
多态的优点
纯虚函数和抽象类
在多态中,通常父类中虚函数的实现是毫无意义的,主要都是调用子类重写的内容。因此可以将虚函数改为纯虚函数
语法:virtual 返回值类型 函数名(参数列表) = 0;
当类中有了纯虚函数,这个类也就称为抽象类
抽象类特点:
虚析构和纯虚析构
多态使用时,如果子类中有属性开辟到了堆区,那么父类指针在释放时无法调用到子类的析构代码
解决方式:将父类中的析构函数改为虚析构或纯虚析构
虚析构和析构的共性:
虚析构和纯虚析构的区别:
虚析构的语法:virtual ~类名(){}
纯虚析构的语法:
class 类名{
virtual ~类名() = 0;
};
类名::~类名(){
析构代码
}
注意:纯虚析构需要声明也要实现,所推荐使用虚析构
程序运行时产生的数据都属于临时数据,程序一旦运行结束就会被释放
通过文件可以将数据持久化
C++中对文件操作需要包含头文件**
文件类型分为两种:
操作文件的三大类:
写文件操作
写文件操作的步骤:
首先要包含头文件
#include
创建流对象
ofstream ofs;
使用流对象打开文件
ofs.open("文件路径", 打开方式);
文件的打开方式:
打开方式 | 解释 |
---|---|
ios::in | 为读文件而打开文件 |
ios::out | 为写文件而打开文件 |
ios::ate | 初始位置:文件尾部 |
ios::app | 追加方式写文件 |
ios::trunc | 如果文件存在,先删除,再创建 |
ios::binary | 二进制方式写文件 |
注意:文件打开方式可以配合使用,需要借助 |
逻辑或运算符 ,例如以二进制方式写文件:ios::binary | ios::out
写数据
ofs << "写入的数据";
关闭文件
ofs.close();
总结:
读文件操作
读文件与写文件步骤类似,但是读取方式相对较多
读文件步骤如下:
包含头文件
#include
创建流对象
ifstream ifs;
打开文件并判断文件是否打开成功
ifs.open("文件路径", 打开方式);
读数据
四种方式读取:
//第一种读方式
char buf[1024] = { 0 };
while( ifs >> buf ) {
cout << buf << endl;
}
//第二种读方式
char buf[1024] = { 0 };
while (ifs.getline(buf, sizeof(buf))) {
cout << buf << endl;
}
//第三种读方式
string buf;
while (getline(ifs,buf))
{
cout << buf;
}
//第四种读方式
char c;
while ((c = ifs.get()) != EOF) {//EOF = End Of File
cout << c;
}
关闭文件
ifs.close();
总结: