C语言是面向过程的,C++是基于面向对象的,关注的是对象。
我们在C语言阶段都接触过结构体,C++兼容C中结构体的用法,struct在C++中升级成了类。看下面代码:
struct Student
{
char name[10];
int age;
int id;
};
int main()
{
struct Student s1; //兼容C
Student s2; //升级到类,Student类名,也是类型
strcpy(s1.name, "张三");
s1.id = 1;
s1.age = 20;
strcpy(s2.name, "李四");
s2.id = 56;
s2.age = 30;
return 0;
}
这是我们熟悉的代码,在定义变量的时候,我们不需要像C语言那样把结构体关键字和名一起写出来,只需要写出类名就可以了。
C++中类和结构体不同的是,类里面不仅可以定义变量也可以定义方法(函数)。
//C++类跟结构体不同的是除了可以定义变量,还可以定义方法(函数)
struct Student
{
//成员变量
char _name[10];
int _age;
int _id;
//成员方法/成员函数
void Init(const char* name, int age, int id)
{
strcpy(_name, name);
_age = age;
_id = id;
}
void Print()
{
cout << _name << endl;
cout << _age << endl;
cout << _id << endl;
}
};
int main()
{
struct Student s1;
Student s2;
s1.Init("张三", 18, 1);
s2.Init("李四", 22, 4);
s1.Print();
s2.Print();
return 0;
}
但是在C++中我们通常使用class,这里我们应该知道面向对象的三大特性是,封装、继承、多态。封装:数据和方法都放在类里面、访问限定符。访问限定符有public、private、protected。
pubilc修饰的成员在类外面可以直接被访问。
private和protected修饰的成员在类外面不可以直接被访问。
访问权限作用域从该访问限定符出现的位置到下一个访问限定符出现为止,如果后面没有访问限定符,作用域就到}即类结束。
//面向对象三大特性:1.封装 、继承 、多态
//封装:数据和方法都放在类里面、访问限定符
class Student
{
private:
char _name[10];
int _age;
int _id;
public:
void Init(const char* name, int age, int id)
{
strcpy(_name, name);
_age = age;
_id = id;
}
void Print()
{
cout << _name << endl;
cout << _age << endl;
cout << _id << endl;
}
};
class Stack
{
public:
void Init()
{
_a = nullptr;
_top = _capacity = 0;
}
void Push(int x)
{
//...
}
int Top()
{
assert(_top > 0);
return _a[_top - 1];
}
private:
int* _a;
int _top;
int _capacity;
};
int main()
{
Student s1;
Student s2;
s1.Init("张三", 18, 1);
s2.Init("李四", 22, 4);
s1.Print();
s2.Print();
Stack st;
st.Init();
st.Push(1);
st.Push(2);
//int top = st.Top();
return 0;
}
类定义了一个新的作用域,类的所有成员都在类的作用域里,在类外面定义成员时,需要用::作用域操作符指明属于哪个类。
//应该在.h头文件中
class Stack
{
public:
void Init();
void Push(int x);
private:
int* _a;
int _top;
int _capacity;
};
//应该在.cpp文件中
void Stack::Init()
{
_a = nullptr;
_top = _capacity = 0;
}
成员函数如果在类里面定义,编译器可能会将它当作内联函数处理。
类就相当于应该模型,需要实例化,用类类型创建对象的过程,称为类的实例化。
如何计算类对象的大小?
//类里面仅有成员函数
class A2
{
public:
void f2()
{}
};
//类里面什么都没有
class A3
{};
//计算类或类对象大小,只看成员变量,考虑内存对齐
//每个对象中都有独立的成员变量
//不同对象调用成员函数,调用的是同一个
int main()
{
A2 aa;
A2 bb;
//空类会给1byte,不存有效数据,占位,表示对象存在
cout << &aa << endl;
cout << &bb << endl;
return 0;
}
计算类或类对象大小,只看成员变量,考虑内存对齐(和C的内存对齐规则一样),因为每个对象中都有独立的成员变量,但是不同对象调用成员函数,调用的是同一个。成员函数放在公共的代码段。
class Date
{
public: //定义的时候不能显示声明形参this
//const修饰this ,不能被修改的this
//void Init(Date* const this, int year, int month, int day)
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
this->_year = year; //传参不能写出来,但里面能用
this->_month = month;
this->_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
d1.Init(2012, 12, 23); //d1.Init(&d1, 2012, 12, 23);
// 调用的时候不能显示传实参给this
return 0;
}
调用的时候不能显示传实参给this ,定义的时候不能显示声明形参this,但是在成员函数内部可以显示地使用。
this存在哪里?形参,一般是存在栈上的,有些编译器放在寄存器中,vs2013,2019是放在寄存器(ecx)中.
下面的两道题目可以验证我们有没有理解:
class A
{
public:
void Show()
{
cout << "Show()" << endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->Show();
return 0;
}
class A
{
public:
void PrintA()
{
cout << _a << endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->PrintA(); //里面解引用了,崩溃
return 0;
}
p虽然是空指针,但是调用成员函数不会编译报错,因为不是语法错误,调用成员函数不会出现空指针的访问,因为成员函数没有在对象里面,把p作为实参传给this指针,如果this解引用,那么程序就会崩溃。
类里面有六个默认成员函数,我们先来看构造函数,构造函数是初始化对象,不是开空间创建对象。它的函数名与类名相同,没有返回值,在对象实例化的时候自动调用,并且可以重载,如果没有显示定义,那么编译器会默认生成,如果显示定义,编译器就不生成。
class Date
{
public:
/*Date()
{
_year = 0;
_month = 1;
_day = 1;
}
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}*/
//语法上无参和全缺省的可以同时存在,但是如果无参调用,就会存在二义性
Date()
{
_year = 0;
_month = 1;
_day = 1;
}
Date(int year = 0, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
//Date d1;
Date d2 = (2012);
Date d3 = (2012, 11);
Date d4 = (2012, 11, 11);
return 0;
}
C++里面把类型分为内置类型和自定义类型
内置类型:int/char/指针/内置类型数组...
自定义类型:struct/clas定义的类型
不写,编译器默认生成构造函数,对内置类型的成员变量不初始化
对自定义类型成员变量会去调用它的默认构造函数初始化,如果没有默认构造函数就报错
默认构造函数:不用参数就可以调用的
默认构造函数:全缺省,无参,不写编译器默认生成的
class A
{
public:
A()
{
cout << "A()" << endl;
_a = 0;
}
private:
int _a;
};
class Date
{
public:
private:
int _year; // 不处理
int _month;
int _day;
A _aa; //调用默认构造函数
};
int main()
{
Date d1;
//d1.Date();
return 0;
}
析构函数是对象销毁时自动调用,完成对象中资源清理工作
析构函数名是类名前面加上~,无返回值,一个类有且仅有一个析构函数,如果不显示实现,编译器会默认生成,对象生命周期结束,编译器自动调用。
class Date
{
public:
Date(int year = 0, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
~Date()
{
//Date类里面没有资源需要清理,所以不实现析构函数是可以的
cout << "~Date()" << endl;
}
private:
int _year;
int _month;
int _day;
};
class Stack
{
public:
Stack(int capacity = 4) //构造函数
{
_a = (int*)malloc(sizeof(int)*capacity);
if (_a = nullptr)
{
cout << "malloc fail\n" << endl;
exit(-1);
}
_top = 0;
_capacity = capacity;
}
void Push()
{}
//不写,默认生成析构函数,和构造函数类似
//对于内置类型不处理
//对于自定义类型会调用它的析构函数
~Stack() //析构函数
{
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
private:
int* _a;
size_t _top;
size_t _capacity;
};
int main()
{
Date d1;
Date d2(2012, 10, 23);
Stack s1;
Stack s2(29);
return 0;
}
默认生成的析构函数,对内置类型的成员变量不处理,对自定义类型的成员变量去调用它的析构函数。在两个栈实现一个队列的题目中,我们就不要在MyQueue类里面定义析构函数。
class Stack
{
public:
Stack(int capacity = 4)
{
_a = (int*)malloc(sizeof(int)*capacity);
if (_a = nullptr)
{
cout << "malloc fail\n" << endl;
exit(-1);
}
_top = 0;
_capacity = capacity;
}
void Push()
{}
~Stack()
{
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
private:
int* _a;
size_t _top;
size_t _capacity;
};
//两个栈实现队列
class MyQueue
{
public:
//默认生成构造函数和析构函数,对自定义类型成员调用它的构造和析构
void push(int x)
{}
private:
Stack pushST;
Stack popST;
};
int main()
{
MyQueue mq;
return 0;
}
拷贝构造函数
拷贝构造函数是构造函数的一种重载,参数有一个,且必须引用传参。传值传参会无穷递归。因为调用拷贝构造需要先传参,传值传参又是一个拷贝构造。自定义类型变量用同一类型初始化,就是拷贝构造。
class Date
{
public:
Date(int year = 0, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//Date d2(d1);
Date(const Date& d) //引用传参
{
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2011, 11, 16);
//拷贝构造
Date d2(d1);
return 0;
}
拷贝构造,我们不写,编译器会默认生成,对自定义类型和内置类型都处理,对内置类型成员,会按字节序拷贝(浅拷贝)(如果指针指向空间的话,会把地址也复制过去,指向了同一块空间),自定义类型成员变量会调用它的拷贝构造。
运算符重载
返回类型 operator 操作符(参数列表)
不能用其他符号创造新的操作符,:: . .* sizeof ?: 这五种不能重载
class Date
{
public:
Date(int year = 0, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//private:
int _year;
int _month;
int _day;
};
//函数类型 operator 操作符
bool operator>(const Date& d1, const Date& d2)
{
if (d1._year > d2._year)
return true;
else if (d1._year == d2._year && d1._month > d2._month)
return true;
else if (d1._year == d2._year && d1._month == d2._month && d1._day > d2._day)
return true;
else
return false;
}
int main()
{
Date d1(2012, 1, 22);
Date d2(2012, 1, 30);
//d1 > d2; -> operator>(d1, d2);
cout << operator>(d1, d2) << endl;
return 0;
}
我们最好将函数定义在类里面
class Date
{
public:
Date(int year = 0, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
bool operator>(const Date& d) //bool operator>(Data* const this, const Date& d)
{
if (_year > d._year)
return true;
else if (_year == d._year && _month > d._month)
return true;
else if (_year == d._year && _month == d._month && _day > d._day)
return true;
else
return false;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2012, 1, 13);
Date d2(2012, 2, 12);
d1 > d2; //d1.operator>(d2)
return 0;
}
赋值运算符的重载
class Date
{
public:
Date(int year = 0, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
Date(const Date& d) //拷贝构造
{
cout << "Date(const Date& d)" << endl;
}
//d1 = d3;
Date& operator=(const Date& d)
{
if (this != &d)
{
_year = d._year;
_month = d._month; //已经完成了内容拷贝
_day = d._day;
}
return *this; //如果是传值返回的话,拷贝给临时变量,是一次拷贝构造
} //如果还有一个变量接收返回值的话,再拷贝构造
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2012, 1, 13);
Date d2(2012, 2, 12);
Date d3(2011, 5, 12);
//一个已经存在的对象拷贝初始化一个马上创建实例化的对象
Date d4(d1); //拷贝构造
Date d5 = d1; //拷贝构造
//两个已经存在的对象之间进行赋值拷贝
d2 = d1 = d3; //d1.operator = (d3)
d1 = d3;
return 0;
}
如果没有显示定义赋值运算符,编译器会默认生成一个,对内置类型的成员变量会按字节序拷贝(浅拷贝),对自定义类型成员变量会调用它的operator=。