1,struct和class区别:struct默认访问权限是public,而class默认访问权限是private(包括成员变量和成员函数),Java中默认是default权限,Java有四种权限,default是包内可以访问,其他包不行;
struct P1{
int i; //默认为public
}
class P2{
int i; //默认为private
}
2,在头文件中加入#pragma once可以防止重复包含,类的结构一般都是声明在头文件中(包括成员变量和函数的声明),其函数实现在cpp文件中:
#include a.h
#include b.h
//如果a.h和b.h中都存在x.h,那么x.h就会包含两次,拖慢编译速度
//解决方式一:
#ifndef _HEADERNAME_H
#define _HEADERNAME_H
...//(头文件内容)
#endif
//当头文件第一次被包含时,它被正常处理,符号_HEADERNAME_H被定义为1。如果头文件被再次包含,通过条件编译,它的内容被忽略。符号_HEADERNAME_H按照被包含头文件的文件名进行取名,以避免由于其他头文件使用相同的符号而引起的冲突。但是第二次头文件仍然会被读入,哪怕里面内容会被忽略,还是会拖慢速度
// #pragma once是windows平台的宏,可以防止重复包含与重打开,直接放文件第一行,一步到位,直接解决
3,构造函数和析构函数:分别用于创建对象时执行初始化操作和对象销毁前执行清理操作,涉及安全问题,如果没有显式给出,编译器会自动加上,但都是空实现;
(1)C++默认提供三个函数:默认无参构造函数、析构函数、默认拷贝构造函数(将所有值拷贝,哪怕将属性声明为私有的也可以拷贝);
(2)如果写了有参构造函数,编译器不再提供默认无参构造函数;
(3)如果写了拷贝构造函数,编译器不再提供其他任何构造函数;
– 构造函数在创建对象时自动调用,可有参数用于初始化,但没有返回值;
– 析构函数语法: ~类名() ,同样没有返回值,也没有参数,在清理对象时自动调用,无需手动调用;
class Person{
private:
int a;
public:
Person(){
}
Person(int a){
this.a = a;
}
Person(const Person &p){ //拷贝构造函数,是Java中的浅拷贝
this.a = p.a;
}
...
}
int main(){
//1,括号法调用构造函数
Person p0; //此处已经为对象p分配内存(在栈区),调用了默认的无参构造函数
Person p1(10); //调用有参构造函数
Person p2(p1); //调用拷贝构造函数
//2,显式法
Person p00;
Person p11 = Person(10);
Person p22 = Person(p11); //拷贝构造
//3,隐式转换法
Person p000 = 10; //等价于Person p000 = Person(10);
Person p111 = p000; //等价于 Person p111 = Person(p000); 会调用拷贝构造函数,因此两个对象实际不在同一片空间,是浅拷贝
}
注意:
(1)调用无参构造方法时不要加括号:Person p();编译器会认为这是一个函数的声明(返回值类型是Person,函数名是p,该函数无参);
(2)Person(10)其意义是创建一个对象,初始化其成员变量a = 10;但这个对象没有名字(匿名对象),在这句执行之后会被马上回收掉;
(3)不要用拷贝构造函数初始化匿名对象:Person (p22); 会报错,这句等价于 Person p22;即新建对象p22,但p22已经被创建,会提示重定义;
(4)拷贝构造相当于Java中的浅拷贝,虽然指向不同的空间,但只是简单类型的值拷贝,指针类型的变量会直接复制地址,等于两个对象中的指针变量,指向的是同一个地址。如何深拷贝?可以在拷贝构造函数中这样写:
class Dog{
...
public:
int *a;
Dog(const Dog &d){
a = d.a; //浅拷贝写法
a = new int(*d.a); //深拷贝写法
}
~Dog(){ //标准的带指针的析构函数写法
if(a != NULL){
delete a;
a = NULL;
}
}
...
}
4,拷贝构造函数的调用时机:
(1)使用已存在的对象创建新对象;
(2)以值传递的方式给函数传参数;
(3)值方式返回局部对象。
疑问:
class Dog{
public:
Dog(){}
Dog(const Dog &d){
cout<<"拷贝函数被调用"<<endl;
}
}
Dog test(){
Dog d;
return d;
}
int main(){
test(); //此行打印一次
test(); //此行打印一次
cout<<"----------------"<<endl;
Dog dog = test(); //此行打印一次
return 1;
}
执行结束,线下面那行为什么不是打印两次?(函数调用打印一次,赋值在C++里表面拷贝构造又一次)线上面调用两次打印两次,表面没有缓存,每次调用函数都会调用拷贝构造函数,因此得出结论:针对此类语句,如果有匿名对象马上被赋给了其他对象,编译器有特殊优化(命名优化(Named Return Value Optimization, NRVO) ),只调用了一次拷贝构造函数。
上面我们说过,不要对匿名对象进行拷贝构造,会重定义,但此处test();不就是给以拷贝构造方式初始化匿名对象吗?这是因为两个对象不在同一个作用域(前者在test()函数中,后者在main()函数中),而且上一个对象也随着函数的结束而销毁,因此没问题。(或者理解为加了const 修饰 const Dog& 绑定 const Dog,没有问题)。
5,静态成员无论变量还是函数都被所有对象共享;静态成员变量在编译时期就已经分配空间,静态成员函数只能访问静态成员变量,不能访问普通成员变量,静态成员变量使用前要必须要在类外初始化:
class Person{
public:
static int ban; //如果在这里初始化 static int ban = 12;也会报错: 带有类内初始化表达式的静态 数据成员 必须具有不可变的常量整型类型,或必须被指定为“内联”
}
int Person::ban = 12; //类外初始化,如果没有这步会报错:无法解析的命令。这个报错通常在链接阶段发生,认为ban变量已经分配内存但还没有初始化。
int main(){
cout<< Person::ban<<endl;
}
一些额外知识:
Person0{
}
Person1{
public:
int m_A;
}
Person2{
public:
int m_A;
static m_B;
void func1(){};
static func2(){};
}
int main(){
Person0 p0;
cout<<sizeof(p)<<endl; //结果是1,类型是空的,对象也是空的,但是为了区分不同的空对象,还是给这些空对象分配一个字节的空间
Person1 p1;
cout<<sizeof(p1)<<endl; //结果是4,非空的类型,里面只有一个int型变量,那就按4分配内存
Person2 p2;
cout<<sizeof(p2)<<endl; //结果是4,函数不论是不是静态的,都是共享一份,与成员变量是分开存储的,注意非静态成员变量也是仅有一份,在调用时,有方式(this指针)用于区分是哪个对象在调用它
}
6,this指针,本质是类型* const this如Person* const this;上面是this指针的用途之一:用于指向调用成员函数的对象,还有一个用途:
Person{
public:
int m_A;
Person& func(){
return *this; //用于返回调用此成员函数的对象本体,如果返回值被声明为Person而不是Person&,那么返回的是一个克隆体(拷贝构造函数调用时机之一)
}
}
注意:
C++中空指针允许访问不属于对象的内容(非静态成员函数、静态成员函数与静态变量):但成员函数中如果出现非静态成员变量,会报错:
Person{
public:
int m_A;
static int ban;
void func0(){
cout<<"abcdefg"<<endl;
}
void func1(){
cout<<m_A<<endl;
}
static void func2(){
cout<<ban<<endl;
}
}
int Person::ban = 12;
int main(){
Person* p = NULL;
p->func0();//abcdefg
p->func1();//报错,访问权限冲突,因为里面涉及非静态成员变量:cout<<m_A<<endl;自动会加上this:cout<< this->m_A <<endl;
p->func2();//12
}
如果想提升代码健壮性,可以这样写:
void func1(){
if (this == NULL) {
return ;
}
cout<<m_A<<endl;
}
7,const修饰成员函数,表示在此函数内,不可以修改非静态成员变量(属于对象的变量),static类型和加了mutable修饰的变量可以修改,使用const修饰对象,那么此对象只能调用const修饰的成员函数和静态函数(即不允许修改属于这个对象的自己的成员变量)
Person{
public:
int id_A;
mutable int id_B;
static int id_C;
void func(){ //随便改
id_A = 10;
id_B = 10;
id_C = 10;
}
void func2() const {
id_A = 20; //报错
id_B = 20; //可以改
id_C = 20; //可以改
}
static func3(){
ban = 20;
}
int Person::id_C = 0;
int main(){
Person p1; //随便调用
const Person p2; //只能调用func2和func3
}
8,友元
大致分三种:
(1)全局函数做友元; 这样写之后全局函数visit可以访问Person的私有属性
(2)类做友元; 这样写之后GoodGay的visit01和visit02都可以访问Person的私有属性
(3)成员函数做友元。 这样写之后GoodGay中只有visit01可以访问Person的私有属性
Person{
public:
friend void visit(); //(1)全局函数做友元写法
friend class GoodGay; //(2)类做友元写法
friend void Goodgay::visit01(); //(3)成员函数做友元写法
Person(){
}
public:
string name;
private:
double money;
}
class GoodGay{
public:
GoodGay(){
}
public:
Person p;
void visit01(){
cout<< p.name <<endl;
cout<< p.money <<endl;
}
void visit02(){
cout<< p.name <<endl;
cout<< p.money <<endl;
}
}
void visit(Person& p){
cout<< p.name <<endl;
cout<< p.money <<endl; //加友元之前报错,加了之后可以访问
}
int main(){
Person p;
visit(p);
}
9,运算符重载:分为全局函数运算符重载和成员函数运算符重载,目前只允许单目和双目运算符重载,而且只能用于自定义类型,简单类型如int、double类型不可以重载。
(1)重载+号与左移运算符<<
以+号(双目运算符)为例:
成员函数运算符重载本质上是p1.operator+(p2);因此单目运算符不能有参数,双目运算符只能有一个参数,因为默认+左操作数使用this指针占了一个;而且可以形成函数重载,如果要加多个参数,可以使用类外函数运算符重载,然后加友元;或者直接使用全局函数重载运算符
p1 + p2 全局函数运算符重载本质上是operator+(p1, p2),也是看几目运算符,单目只能有一个参数,双目就只能有两个参数,不能多也不能少,如果只写一个参数 Person operator+(Person &p),那么其应该这么调用:+p1;不知道有啥用
class Person{
public:
int m_A = 10;
int 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;
}
Person operator+(int num){ //重载运算符+ 同时也与上面的函数形成函数重载
Person temp;
temp.m_A = this->m_A + num;
temp.m_B = this->m_B + num;
return temp;
}
}
//如果把成员函数重载运算符注掉,也可以用全局函数重载运算符
Person operator+(Person &p1, Person &p2){
cout<< "全局函数重载运算符" <<endl;
Person temp;
temp.m_A = p1.m_A + p2.m_A;
temp.m_B = p1.m_B + p2.m_B;
return temp;
}
//重载左移运算符<<,cout是对象名,其类型是ostream(标准输出流),全局只能有一个,起名就叫cout,因此必须以引用方式使用(不然做形参或者返回值时会调用拷贝构造函数创建副本),另外cout<<的返回值还是cout,这也是为什么可以链式编程cout<<值1<<值2 ,如果返回值不是cout,那就不能链式编程,只能输出一个值cout<<值1,如果值是peivate的还要加上友元
//由于要求cout必须在左,要输出的值在右,因此必须以全局函数形式重载,因为cout是左操作数,<<是双目运算符,如果是成员函数形式重载
//那么要输出的值是就必须是左操作数,cout是形参,如下
//ostream& operator<<(ostream &cout){},
ostream& operator<<(ostream &cout, Person &p){
cout<< "m_A: "<< p.m_A << "m_B: " << p.m_B;
return cout;
}
void test01(){
Person p1;
Person p2;
Person p3 = p1 + p2; //本质是: p1.operator+(p2),可以简写成p1 + p2;
Person p4 = p1 + 20; //本质是:p1.operator+(20),可以简写成p1 + 20;
//把成员函数重载运算符去掉后,使用全局函数重载运算符
Person p5 = p1 + p2; //会打印:全局函数重载运算符,本质是:Person p5 = operator+(p1, p2);
cout << p1 <<endl;
}
int main(){
test01();
system("pause");
return 1;
}
(2)自增运算符的重载:
class Person{
friend ostream& operator<<(ostream& cout, Person p);
public:
//重载前置++ 如++p, 这里返回Person引用类型是为了链式编程
Person& operator++(){
this->height++;
return *this;
}
//重载后置++ 如p++ 这里加个int是为了占位,防止函数重定义,也让编译器识别这是后置++的重载
//这里返回Person类型而不是引用类型,是因为temp是局部变量,不能返回引用类型,如果开辟在堆区,就可以返回引用类型了
Person operator++(int){
Person temp = *this;
this->height++;
return temp;
}
private:
int height = 180;
}
//重载左移运算符<<
ostream& operator<<(ostream& cout, Person p){
cout << p.height;
}
int main(){
Person p;
cout << p++ << endl; //180
cout << ++p << endl; //182
}
这里碰到一个坑,就是如果重载左移运算符时,第二个参数声明为引用类型,那么cout<<p++<<endl;就会报错:
class Person{
friend ostream& operator<<(ostream& cout, Person& p);
...
}
ostream& operator<<(ostream& cout, Person& p){
cout << p.height;
}
int main(){
Person p;
cout << p++ << endl; //报错:二元“<<”: 没有找到接受“Person”类型的右操作数的运算符(或没有可接受的转换
}
不明白为啥报错,难道是因为 p++ 的返回值是个Person而不是Person引用?那之前学习时以下代码为什么不报错?在传参时不是自动将普通类型转成引用类型了吗?请大佬解答
void add(int &a){
a += 20;
}
int main(){
int a = 10;
add(a);
cout << a << endl; //30
}
得到的答案是:
p++是一个临时对象,在C++中临时对象都是不可修改的,是const Person类型,而引用类型绑定的对象,必须都可以通过该引用来修改,因此报错,就像这样:
int a = 10;
int& b = a; //成功,a可通过b来修改,例如b = 20
const int c = 10;
int& d = c; //失败,因为c被const修饰,不可修改,如果要绑定,就必须写成 const int& d = c
这里p++就是一个临时对象,如同上例的c一样被const修饰,不可以直接绑定到引用上,可以加const来绑定:
ostream& operator<<(ostream& cout, const Person& p)
(3)重载赋值运算符=
编译器给每个类默认提供四个函数:
默认构造,默认拷贝构造,默认析构,第四个就是默认的重载赋值运算符=
class Person{
public:
int* m_A;
Person(const Person& p){
cout<<"拷贝构造函数调用"<<endl;
this->m_A = p.m_A;
}
Person& operator=(Person& p){ //重写了 operator=函数,使其变成深拷贝,另外返回值是*this,为了链式编程
this->m_A = new int(*p.m_A);
return *this;
}
~Person(){
if(m_A != NULL){
delete m_A;
m_A = NULL:
}
}
}
int main(){
Person p1;
Person p2;
p2 = p1;//这句会打印 拷贝构造函数调用,表明是重载的赋值运算符=函数里,其实是调用了拷贝构造函数
Person p3;
p3 = p2 = p1; //operator=返回*this,并且是引用类型,就是为了这样的链式编程
return 1;
}
(4)重载关系运算符==和!=,自定义类型的比较,不多赘述
(5)重载小括号() 这称之为仿函数:
class Person{
public:
void operator()(){ //第一个小括号是重载的运算符,第二个小括号是参数列表
cout << "没有参数的仿函数" <<endl;
}
void operator()(string str){
cout << str << endl;
}
}
int main(){
Person p;
p("abc"); //使用起来非常像个函数,因此称为仿函数,调用第二个
p(); //调用第一个
Person()(); //Person()匿名对象,调用第一个
Person()("efg"); //Person()匿名对象,调用第二个
return 1;
}