《黑马程序员》学习记录
define
宏常量:#define A 10
const
修饰的变量:const int A = 10;
默认情况下,C++ 只会输出六位有效数字的小数!!!
char a = '1';
string a = "1234"
(C++风格),char str[] = "1234"
(C语言风格)C++ 风格的字符串需要引用头文件
# include
cin >> a;
cout << (a==b) << endl;
输出表达式时要带括号;true/false
(这跟Python中and/or/not
有些区别)C++:
3 && 2
返回1
;Python:3 and 2
返回 2!
a>b?a:b;
它返回的是变量,不是变量的值,a>b?a:b=100;
是正确的;swicth...case...break...default
,尤其注意不能少了 break
;if 和 switch 的区别:
(1)if 可以判断区间,switch 只能判断整型或字符型,不能是区间;
(2)switch 结构清晰,执行效率比 if 高;
Switch 不允许一个 case 使用另一个 case 后声明定义的变量,错误信息为
jump bypasses variable initialization
!
cout << "aaa" << endl;
goto FLAG;
cout << "bbb" << endl; // 被跳过
cout << "ccc" << endl; // 被跳过
FLAG;
cout << "ddd" << endl;
数组的内存是连续的!
一维数组
// 三种定义方式
int a[10];
int b[10] = {0, 1, 2, 3};
int c[] = {0, 1, 2, 3};
数组名的作用:
查看数组的内存大小:sizeof(a)
查看数组的地址:a
返回的就是数组的首地址,也就是 a[0]
的地址;a == &a[0]
;
注意:数组名是常量(也就是数组的地址),不能修改;a=100
是错误用法!
数组元素个数(长度):int a_length = sizeof(a) / sizeof(a[0]);
;字符数组 char a[] = "123"
可以用 strlen
;
二维数组
// 四种定义方式
int a[2][3];
int b[2][3] = {{0, 1, 2}, {3, 4, 5}};
int c[2][3] = {0, 1, 2, 3, 4, 5};
int d[][3] = {0, 1, 2, 3, 4, 5};
sizeof(a)
a
返回的就是二维数组的首地址,也就是 a[0][0]
的地址;a == a[0], a == &a[0][0]
;int a_row = sizeof(a) / sizeof(a[0]);
int a_col = sizeof(a[0]) / sizeof(a[0][0]);
main()
中用到的函数必须在它之前有声明或者定义(跟Python不一样),否则报错!可以多次声明。xxx.h
,引入 iostream, std
等xxx.cpp
,引用头文件int main()
,引用头文件int a = 10;
int *p = &a; // 定义,赋值
*p = 20; // 使用指针(解引用:在指针变量前面加一个 * 就是解引用)
cout << (long)pt << endl;
)int *p1 = NULL;
此时 p==0
;内存编号 0~255 是系统内存,不允许访问int *p = (int *)0x1100;
int a = 10;
int b = 20;
int c = 30
const int * p1 = &a; // 常量指针,指向的值不能改
int * const p2 = &b; // 指针常量,指针的指向不能改
const int * const p3 = &c; //
int (*p)[5];
,数组的指针(()
的优先级比[]
高,所以p
会先跟*
结合,构成一个指针)int *p[5];
,由指针组成的数组([]
的优先级比*
要高,所以p
会先跟[]
结合,构成一个数组)// 数组跟指针的关系
int a[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int *p = a; // 指针指向数组,也就是第一个元素
cout << *p << endl;
p++; // 指针指向第二个元素
cout << *p << endl;
// 数组指针,指针数组
int (*p1)[5]; // 数组指针,数组的指针
int *p2[5]; // 指针数组,数组中每一个元素都是指针
void func(int *arr)
,定义一个指针即可func(arr)
,直接传入数组名(也就是数组地址)即可arr[i]
,将指针当做数组名一样使用即可(但是无法使用sizeof(arr)
获取数组内存大小)struct Student
{
string name;
int grade;
double math;
};
// 方式一:
Student a;
a.name = "张三";
a.grade = 2;
a.math = 92.5;
// 方式二:
Student b = {"李四", 3, 85.5};
// 方式三:在结构体定义的最后创建变量
.
而是->
.
和->
的联系和区别:(1)联系:
.
和->
都可以用来访问成员(2)区别:
.
是实体对象访问成员用的,左边必须是实体对象;->
是指针访问成员用的,左边必须是指针;
// new int(10) 创建整型数据,放在堆区,返回数据的指针
// int *p 是一个整型指针,放在栈区,指向堆区变量
int *p = new int(10); // new 创建变量
int *arr = new int[10]; // new 创建数组
不要返回局部变量的地址!!!编译器在局部变量使用完以后,会保留一次局部变量的使用权,所以在外部第一次使用局部变量的地址时,不会出错;但是这一次机会用完以后,放在栈区的局部变量就被销毁掉了,第二次再使用就会出错!
new/delete
的使用:
new
返回的是所创建的数据的指针,可能是整数的指针,也可能是数组的指针delete
释放数组需要加一个 []
delete p;
delete[] arr; // 堆区数组的释放要加一个 []
int a = 10;
int &ref_a = a; // a 的引用
int *const pt_a = &a; // a 的指针常量,其实就是引用,它的值跟 &a、&ref_a 是完全一致的
int a = 10;
int &b = a; // b 是 a 的别名
int c = 20;
b = c; // 这个并不是改变引用的对象,而是一个赋值操作,将 b 所代表内存中的数据改为 c 的值,之后 a/b/c 都等于 20
地址传递:变量,独立;可变,可空;替身,无类型检查;
引用传递:别名,依赖;不变,非空;本体,有类型检查;
总结:引用传递和地址传递,函数形参如何定义?函数调用如何传参?
关键:形参定义方式跟引用、指针的定义一样,函数传参方式跟引用、地址的赋值一样!!!
实例:
- 引用传递:
- 形参定义:
int function(int &a)
- 函数传参:
function(aa)
,接收参数就类似于int &a = aa;
,这其实就是引用的定义与赋值;- 地址传递:
- 形参定义:
int function(int *a)
- 函数传参:
function(&aa)
,接收参数就类似于int *a = &aa;
,这其实就是地址的定义与赋值;
int &ref_a = 10; // 这是错误的,不能直接引用一个数值,数值是在常量区,不在栈区,也不在堆区
const int &ref_a = 10; // 这是对的,它相当于创建了一个缓存变量 tmp,再引用它:int tmp=10; const int &ref_a=tmp;
int func(int a, int); // 这里的第二个参数就是占位参数
func(10, 20); // 调用时必须传实参
int func1(int a, int = 10); // 占位参数的默认值
void func(int &a)
与 void func(const int &a)
构成函数重载void func(int a)
与 void func(int a, int b=10)
不能构成函数重载C++ 面向对象三大特性:封装,继承,多态!!!
权限问题:
public
:类内可以访问,类外也可以访问private
:类内可以访问,类外不可以访问protect
:类内可以访问,类外不可以访问protected
跟private
的区别体现在继承中,子类可以访问父类的protected
成员,但不可以访问private
成员class
与struct
的区别:class
默认权限是private
,struct
默认权限是public
运算符重载:bool operator==(const Cube &c);
Person();
Person(const Person &p);
,它的调用时机有以下三种:
Person p2(p1);
Person p1; // 调用默认构造函数
Person p2(); // 编译器会认为是一个函数的申明,而不是调用默认构造函数
// 1. 括号法
Person p2(10); // 调用有参构造函数
Person p2(p1); // 调用拷贝构造函数
// 2. 隐式法
Person p3 = 10; // 调用有参构造函数
Person p3 = p2; // 调用拷贝构造函数
// 3. 显式法
Person p3 = Person(10); // 调用有参构造函数
Person p3 = Person(p2); // 调用拷贝构造函数
// 注意事项:
Person(10); // 匿名对象,在此行执行完后会立即调用析构函数,销毁该内存
Person(p3); // 错误,编译器会将其视为 Person p3; ,而 p3 在上面定义过了,所以会报错
// 因此,不要用拷贝构造函数初始化一个匿名对象!!!
创建一个类,编译器至少会添加四个函数:
- 默认构造函数(无参,函数体为空)
- 默认析构函数(无参,函数体为空)
- 默认拷贝构造函数(对属性值进行拷贝)
- 赋值运算符
operator=
,对属性值进行拷贝但是,如果自定义了有参构造函数,编译器则不会提供默认构造函数,但是会提供拷贝构造函数,此时
Person p;
报错;如果自定义了拷贝构造函数,编译器不再提供任何构造函数,需要自定义有参构造函数、默认构造函数;
当A类对象作为B类属性时,创建一个B类对象,会先调用A类构造函数,再调用B类构造函数;析构函数则相反!
private
静态成员变量在类外无法直接访问,public
可以class Person{
public:
static int m_A; // 类内声明;权限为 public
}
int Person::m_A = 10; // 类外初始化
int main(){
Person p;
cout << p.m_A << endl; // 通过对象访问
cout << Person::m_A << endl; // 通过类访问
return 0;
}
class Person{
public:
static int test_static_value;
static void test_static_func(){
cout << test_static_value << endl; // 这里不能使用 this 指针,this 指针只能用于非静态成员函数内部
}
}
this
指针p
是空对象,那么 sizeof(p)
就等于 1;sizeof
函数可以查看对象的大小,非空对象的大小只跟它内部的非静态成员变量大小有关;class Person{
public:
int aa;
}
Person p;
cout << sizeof(p) << endl; // 输出为4,因为对象 p 内部只有一个整型的非静态成员变量
非静态成员函数只有一份函数实例,存在代码区,所有的对象共用这一个函数实例,那如何区分是哪个对象调用了的这个非静态成员函数呢?就是用 this
指针去区分的。
this
指针指向被调用非静态成员函数所属的对象,也就是说:如果有 Person p;
这样一个对象,当用 p
调用非静态成员函数 func()
时,func()
内部会有一个默认的 this
指针,它指向对象 p
(不会指向其他的 Person
对象)。
this
指针的作用:
*this
,返回类型可以是值,也可以是引用,二者是有区别的)空指针调用成员函数:成员函数如果使用了成员变量,此时this
指针会为空,导致调用失败;一般的解决方式是,在成员函数中加一个指针为空的判断,使得成员函数更加健壮。
class Person{
public:
int age;
void func(){
if(this == NULL){ // 判断this指针是否为空
return; // 如果不加这个判断,后面空指针调用该成员函数时,下面的代码会崩掉
}
cout << this->age << endl; // 用到了成员函数
}
}
Person *p = NULL;
p.func();
const
修饰的成员函数,其内部的成员属性不能修改,除非使用关键字mutable
修饰
const
修饰的是this
指针的指向,本来this
指针就是指针常量,也就不能修改它的指向,现在再用一个const
修饰它的指向,也就是说它指向的值也不能修改,即成员变量不能修改!const
修饰,它只能调用常函数,只能修改mutable
修饰的成员变量;class Person{
public:
int m_A; // 常函数中不可修改的成员变量
mutable int m_B; // 常函数中可以修改的成员变量
void func() const{ // 常函数
this->m_B = 10; // 常函数中只能修改有mutable的成员变量
}
}
const PersonX p; // 常对象
p.m_B = 10; // 常对象可以修改有mutable的成员变量
p.func(); // 常对象只能调用常函数
让函数和类,能够访问另一个类的私有成员;使用 friend
关键字
friend
修饰friend
修饰friend
修饰;class Building
{
friend void building_friend(Building b); // 友元全局函数
friend class GoodGay01; // 友元类
friend void GoodGay02::visit(); // 友元成员函数
private:
string bed_room;
public:
string setting_room;
Building();
~Building();
};
class Building;
class GoodGay
{
private:
Building *b; // 这个地方只能用指针或者引用,在构造函数中相应的要用new手动创建对象
public:
GoodGay();
~GoodGay();
void visit();
};
+
Person operator+(Person p){...} // 成员函数重载,跟类有关,本质是 p1.operator+(p2)
Person operator+(Person p1, Person p2){...} // 全局函数重载,跟类无关,本质是 operator+(p1, p2)
<<
cout << ... << endl;
;<<
,因为这样没办法实现cout
在左边,所以只能用全局函数来重载<<
;<<
时,又不能访问类中的私有成员变量,所以要将全局函数变为友元;# 在.h中写全局函数的声明
class Person{
friend ostream &operator<< (ostream &cout, Person &p); // 将重载函数变为类的友元,方便访问私有成员
Private:
int m_A;
int m_B;
}
# 在.cpp中写全局函数的实现
ostream &operator<< (ostream &cout, Person &p){ // 返回类型是ostream,这是输出流对象的类型
cout << "m_A: " << p.m_A << " m_B: " << p.m_B; // 自定义输出内容、形式,也就是重载的核心功能
return cout; // 将cout返回,方便链式输出
}
++
Person &operator++();
Perosn operator++(int);
class Perosn{
private:
int m_A;
int m_B;
public:
Perosn &operator++(); // 前置递增运算符重载,返回的是引用,形参为空
Person operator++(int); // 后置递增运算符重载,返回的是值,形参需要一个int占位参数(声明为后置),且必须是int
}
// 前置递增运算符重载的实现
PersonX &PersonX::operator++()
{
++(this->m_A); // 属性递增
++(this->m_B); // 属性递增
return *this; // 返回自身的引用
}
// 后置递增运算符重载的实现
PersonX PersonX::operator++(int)
{
PersonX tmp = *this; // 先保存原始值,后面需要返回
(this->m_A)++; // 属性递增
(this->m_B)++; // 属性递增
return tmp; // 返回原始值
}
tmp
,它只能是值传递,不能是引用传递;friend ostream &operator<<(ostream &cout, Person p);
,不能用Perosn &p
;=
如果有成员是在堆上创建的,就必须让赋值操作符重载,因为默认的赋值操作符是浅拷贝,会发生内存泄漏;
赋值操作符重载形式:Perosn &operator=(Perosn &p);
;
class Person{
private:
int *m_A;
public:
Person(int a){
m_A = new int(a);
}
Person &operator=(Perosn &p); // 赋值运算符重载定义
}
Person &operator=(Person &p){
if (this->m_A != NULL){
delete this->m_A; // 一定要注意,如果堆区内存还存在,要先释放,防止内存泄漏
}
this->m_A = new (*p.m_A); // 将赋值操作转移到堆区
return *this; // 返回自身,方便进行链式赋值(a=b=c)
}
bool operator==(Person &p);
()
// 案例一:打印类的函数调用运算符重载
class MyPrint{
public:
void operator()(string s){
cout << s << endl;
}
}
MyPrint mp;
mp("hello world!"); // 因为它的使用方式很像函数调用,但实际上它是一个运算符,所以叫做仿函数
// 案例二:加法类的函数调用运算符重载
class MyAdd{
public:
int operator()(int a, int b){
return a + b;
}
}
int a = b = 10;
MyAdd md;
int c = md(a, b); // 两个形参,返回整型值
基本语法:class FootballPlayer : public PersonX
,class 子类 : 继承方式 父类
继承方式:
public
还是public
,protect
还是protect
,private
不可访问public
变为protect
,protect
还是protect
,private
不可访问public
还是private
,protect
变为private
,private
不可访问继承中的对象模型:
sizeof()
查看VS 中可以使用 cl 工具看对象内存模型(参考,
cl /d1 reportSingleClassLayout类名 文件名
),Mac+VSCode 要怎么看???
继承中的构造和析构顺序:先调用父类构造函数,再调用子类构造函数;析构顺序相反。
继承中同名成员处理:
.
出来即可,son.age
son.Base::age
多继承语法:class 子类 : 继承方式 父类1, 继承方式 父类2, ...
菱形继承:
vbtable
和vbptr
来访问)
vbtable
:虚基类表vbptr
:虚基类指针,指向vbtable
// 菱形继承
class Animal{
public:
int m_Age;
}
class Sheep : public Animal{}
class Tuo : public Animal{}
class SheepTuo : public Sheep, public Tuo{}
SheepTuo st;
st.Sheep::m_Age = 18; // 菱形继承时需要加作用域才能访问继承成员
st.Tuo::m_Age = 28; // 此时 st 中有两份 m_Age,分别来自于 Sheep、Tuo
// 虚继承解决菱形继承的问题
class Animal{
public:
int m_Age;
}
class Sheep : virtual public Animal{} // 虚继承
class Tuo : virtual public Animal{} // 虚继承
class SheepTuo : public Sheep, public Tuo{}
SheepTuo st;
st.Sheep::m_Age = 18; // 第一次修改 st 的 m_Age
st.Tuo::m_Age = 28; // 第二次修改 st 的 m_Age,修改的是同一份数据
多态的类型:
动态多态:
class Animal
{
public:
virtual void speak(); // 虚函数
};
class Cat : public Animal
{
public:
void speak(); // 重写虚函数,不需要再加virtual
};
class Dog : public Animal
{
public:
void speak(); // 重写虚函数,不需要再加virtual
};
首先父类中有虚函数存在时,其对象就会有一个vfptr
,指向vftable
中的虚函数入口地址
vfptr
:虚函数指针,属于对象内存模型中的一部分vftable
:虚函数表,里面存储的是虚函数的入口地址(父类是父类的,子类是子类的)其次,当子类继承父类时,它也会继承vfptr
,但是这个vfptr
指向的是子类的vftable
,不再是父类的vftable
,它的实现也就是子类的实现(也就是,子类重写虚函数时,它的vftable
就会替换成子类虚函数实现的入口地址)
从下面的对象模型可以看到:
vfptr
vfptr
,它指向的还是父类的vftable
,函数实现也是父类的vftable
就会被替换成子类自己的虚函数表,其函数实现就是子类自己的纯虚函数和抽象类:
=0
,不需要实现虚析构和纯虚析构:
虚析构:析构函数定义为虚函数
使用场景:子类中有成员创建在堆区,且父类指针指向子类对象,当手动delete
父类指针时,父类指针无法调用子类析构函数,子类对象在堆区的成员就无法被释放掉,会造成内存泄露
使用方式:将父类析构函数定义为虚函数
注意事项:抽象类(有纯虚函数的父类)的析构函数不能是虚函数(只能是纯虚函数???)
class Animal
{
public:
Animal()
{
cout << "Animal构造函数" << endl;
}
virtual ~Animal() // 虚析构
{
cout << "Animal析构函数" << endl;
}
virtual void speak(){}; // 虚函数
};
Animal *animal = new Cat("Tom"); // 父类指针指向子类对象(在堆区)
animal->speak(); // 调用子类重写的虚函数
delete animal; // 手动释放内存(不然无法调用析构函数)
纯虚析构:析构函数定义为纯虚函数
class Animal
{
public:
Animal()
{
cout << "Animal构造函数" << endl;
}
virtual ~Animal() = 0; // 纯虚析构
virtual void speak() = 0; // 纯虚函数
};
// 纯虚析构必须要写函数实现
Animal::~Animal()
{
cout << "Animal析构函数" << endl;
}
C++ 操作文件需要包含 fstream
头文件,主要有三个大类:
ofstream
:将数据写入文件操作,out
输出到文件#include
ofstream ofs;
ofs.open("文件路径", 打开方式);
ofs << "写入内容" << endl;
ofs.close();
ifstream
:从文件中读取数据#include
ifstream ifs;
ifs.open("文件路径", 打开方式);
if(ifs.is_open()){
// 四种读取方式
ifs.close();
}
// 第一种读取方式
char buf[1024] = {}; // 读取到字符数组
while (ifs >> buf) // 逐行读入到buf中,读完返回false
{
cout << buf << endl;
}
// 第二种读取方式
char buf[1024] = {};
while (ifs.getline(buf, sizeof(buf)))
{
cout << buf << endl;
}
// 第三种读取方式
string buf; // 读取到字符串
while (getline(ifs, buf)) // 逐行读取
{
cout << buf << endl;
}
// 第四种读取方式
char c; // 读取到字符中
while ((c = ifs.get()) != EOF) // 逐个字符读取
{
cout << c;
}
fstream
:读写文件数据文件打开方式:打开方式可以配合使用,使用 |
连接就可以
打开方式 | 解释 |
---|---|
ios::in | 以只读方式打开文件 |
ios::out | 以写入方式打开文件 |
ios::ate | 打开文件,且初始位置在文件尾 |
ios::app | 以追加方式写入文件 |
ios::trunc | 如果文件存在,则先删除再创建 |
ios::binary | 二进制方式 |
打开方式指定为ios::binary
,写入方式使用成员函数ostream& write(const char *buffer, int len);
。
ofstream ofs;
ofs.open("test04.txt", ios::binary | ios::out);
PersonX p("程", 28, 173);
ofs.write((const char *)&p, sizeof(p));
ofs.close();
读二进制文件使用成员函数istream& read(char *buffer, int len);
。