站在编译器和C的角度剖析c++原理, 用代码说话
this强化
我们上一篇中最后提到了this指针的本质,这次我们稍微做一下回顾和强化:
class Test
{
public:
Test(int a, int b) //有参构造函数
{
this->a = a;
this->b = b;
}
int getA()
{
return a;
}
int getB()
{
return b;
}
public:
Test& add(Test &t2) //*this
{
this->a = this->a + t2.getA();
this->b = this->b + t2.getB();
return *this; //*操作让this指针回到元素状态
}
Test add2(Test &t2) //*this
{
Test t3(this->a+t2.getA(), this->b + t2.getB()) ;//调用了构造器,初始化了Test
return t3;//返回Test对象
}
public:
void printAB()
{
cout<<"a:"<<a<<"b:"<<b<<endl;
}
protected:
private:
int a;
int b;
};
//全局函数
//如果把全局函数转成成员函数,少了一个操作数,通过this指针,被隐藏。
Test add(Test &t1, Test &t2)
{
Test t3(t1.getA()+t2.getA(), t1.getB() + t2.getB());
return t3;//返回的是t3对象,赋值给匿名对象。
}
//把成员函数转成全局函数,需要多一个参数。。。。
void printAB(Test *pthis)
{
cout<<"a:"<<pthis->getA()<<"b:"<<pthis->getB()<<endl;
}
int main()
{
Test t1(1, 2); //Test(&t1, 1, 2);
Test t2(3, 4);
Test t3 = add(t1, t2);
t1.add(t2); //---->add(&t1, t2);
t1.printAB();
return 0;
}
我们可以看出当我们使用Test t1(1, 2);
去调用类的构造函数初始化的时候,如果形参和类的私有属性没有重名的话可以不用this的,但是我们要清楚本质,所以开始学习这些的时候都写全吧. 总结一点就是说,当我们调用类中的成员方法,不管是构造函数还是什么,都会隐藏的传入this指针的.
货物链表类
我们来进行一个小型的综合案例. 这个例子就是说维护一个链表,当你补充的时候就在链表尾不断的加,如果买走一个,就是头节点走,然后往后移动:
class Goods
{
public:
Goods ( int w) {
weight = w ;
total_weight += w ;
}
~Goods() {
total_weight -= weight ;
}
int Weight() {
return weight ;
};
static int TotalWeight() {
return total_weight ;
}
Goods *next ;
private:
int weight ;
static int total_weight ;
};
int Goods::total_weight = 0 ;
//业务操作函数 通过全局函数实现
void purchase( Goods * &f, Goods *& r, int w )
{
Goods *p = new Goods(w) ; //用new 在堆中创建。
p -> next = NULL ;
if ( f == NULL ) {
f = r = p ;
}
else {
r -> next = p ;
r = r -> next ;
}
}
//业务操作函数
void sale( Goods * & f , Goods * & r )
{
if ( f == NULL ) { cout << "No any goods!\n" ; return ; }
Goods *q = f ; f = f -> next ; delete q ;//从堆上删除。
cout << "saled.\n" ;
}
int main()
{
Goods * front = NULL /*头*/, * rear = NULL ;
int w ; int choice ;
do
{
cout << "Please choice:\n" ;
cout << "Key in 1 is purchase,\nKey in 2 is sale,\nKey in 0 is over.\n" ;
cin >> choice ;
switch ( choice ) // 操作选择
{
case 1 : // 键入1,购进1箱货物
{
cout << "Input weight: " ;
cin >> w ;
purchase( front, rear, w ) ; // 从表尾插入1个结点
break ;
}
case 2 : // 键入2,售出1箱货物
{
sale( front, rear ) ;
break ;
} // 从表头删除1个结点
case 0 :
break ; // 键入0,结束
}
cout << "Now total weight is:" << Goods::TotalWeight() << endl ;
} while ( choice ) ;
return 0;
}
我们会发现没有拷贝构造函数,那么在进行引用传递的时候,只能进行浅拷贝,但是我们进行的是指针的引用传递, 当作用域结束后,栈中的指针就会释放掉了, 所以能够修改也不影响什么.
友元函数
什么是友元函数,这是一种用friend
关键词定义的函数,说白了就是该类外的函数无法使用到该类的私有变量,但是在类中用friend
申明后就表示是类的好朋友了,就能够使用类中的私有属性了:
class A1
{
public:
A1()
{
a1 = 100;
a2 = 200;
}
int getA1()
{
return this->a1;
}
//声明一个友元函数
friend void setA1(A1 *p, int a1);
protected:
private:
int a1;
int a2;
};
void setA1(A1 *p, int a1)
{
p->a1 = a1;
}
int main()
{
A1 mya1;
cout<<mya1.getA1()<<endl;
setA1(&mya1, 300);
cout<<mya1.getA1()<<endl;
return 0;
}
应为既然用到友元,那么一般就和类有关系了,所以在友元函数的参数中一般会传入类.
友元函数破坏了类的封装性, 为什么要定义个这东西呢?在java中如果想要访问私有属性,可以通过反射机制, 但是对于c++来说,这个难度很大,因为: java的编译过程是: java—》1.class==》class==>java类, 但是c++的编译过程是: cc++ 1预编译gcc -E 2汇编 gcc -i 3编译gcc -c 3、链接ld ===》汇编代码.
我们再来个友元类的例子:
class A
{
//b是a的好朋友
friend class B;
public:
void display()
{
cout<<x<<endl;
}
protected:
private:
int x;
};
class B
{
public:
void setA(int x)
{
Aobj.x = x;//在B类中能直接使用A类的私有成员
}
void printA()
{
cout<<Aobj.x<<endl;
}
protected:
private:
A Aobj;
};
int main()
{
B b1;
b1.setA(100);
b1.printA();
return 0;
}
操作符重载
class Complex{
public:
Complex(int a, int b){
this->a = a;
this->b = b;
}
private:
int a;
int b;
};
int main(void){
Complex c1(1, 2), c2(3, 4);
int a = 0;
int b = 0;
int c = a + b;
Complex c3 = c1 + c2;
return 0;
}
首先对于c来说是没问题的,但是c3是编译不通过的,这是为什么? 因为编译器对基础数据类型知道如何去运算的,但是对于用户自定义类型编译器并不知道如何去进行加减. 所以我们必须对这些自定义类型使用的操作符进行重载. 为了方便起见我们先将私有属性移到public区:
class Complex{
public:
Complex(int a, int b){
this->a = a;
this->b = b;
}
int a;
int b;
private:
};
Complex add(Complex &c1, Complex &c2){//通过全局函数完成对象加减
Complex c3(c1.a + c2.a, c1.b + c2.b);
return c3;
}
int main(void){
Complex c1(1, 2), c2(3, 4);
int a = 0;
int b = 0;
int c = a + b;
//Complex c3 = c1 + c2;
Complex c3 = add(c1, c2);
return 0;
}
这样我们的c3就没问题了,就是写了个全局方法,那么这还只是真正操作符重载路上的一步,接下来我们就进一步演变:
class Complex{
public:
friend Complex operator+(Complex &c1, Complex &c2);
Complex(int a, int b){
this->a = a;
this->b = b;
}
private:
int a;
int b;
};
Complex operator+(Complex &c1, Complex &c2){
Complex c3(c1.a + c2.a, c1.b + c2.b);
return c3;
}
int main(void){
Complex c1(1, 2), c2(3, 4);
int a = 0;
int b = 0;
int c = a + b;
Complex c3 = c1 + c2;
//Complex c3 = add(c1, c2);
return 0;
}
我们首先将属性恢复了正常, 然后我们写了一个全局的operator+
的方法,因为要用到类中的属性,所以在类中建立好朋友关系. 这就是加号的重载就完成了. Complex c3 = c1 + c2;
的本质就是Complex c3 = operator+(c1, c2);
直接返回了匿名对象,用匿名对象进行初始化,不需要走拷贝构造和构造效率瞬间提高. 也就是我们之前说到的,这种方式相当于c3接手了函数作用域中的c3,而不仅仅是浅拷贝了. 如果是先定义了c3然后再从匿名函数赋值,那就是浅拷贝了,如果里面有指针指向堆中就存在很大的问题了.
我们认识了操作符重载本质就是函数后,我们来进行”*”和前置”++”的重载, 在实际中,这些简单操作符都作为成员函数存在的, 所以我们全局函数和成员函数都试验一下:
class Complex
{
public:
friend Complex operator+(Complex &c1, Complex &c2);
friend Complex operator*(Complex &c1, Complex &c2);
friend Complex& operator++(Complex &c1); //前置++
Complex(int a, int b)
{
this->a = a;
this->b = b;
}
void printCom()
{
cout<<a<<" + "<<b<<"i "<<endl;
}
//通过类成员函数完成-操作符重载
Complex operator-(Complex &c2)
{
Complex tmp(a - c2.a, this->b - c2.b);
return tmp;
}
//通过成员函数完成前置--
Complex& operator--()
{
this->a--;
this->b--;
return *this;
}
private:
int a;
int b;
};
Complex operator+(Complex &c1, Complex &c2)
{
Complex tmp(c1.a + c2.a, c1.b + c2.b);
return tmp;
}
Complex operator*(Complex &c1, Complex &c2)
{
Complex tmp(c1.a*c2.a, c1.b*c2.b);
return tmp;
}
//前置++
Complex& operator++(Complex &c1)
{
c1.a ++;
c1.b ++;
return c1;
}
int main()
{
Complex c1(1, 2), c2(3, 4);
Complex c3 = c1 + c2;
//用类成员函数实现-运算符重载
Complex c4 = c1 - c2;
c4.printCom();
//等价于Complex c4 = c1.operator-(c2);
//前置++ 全局函数
++c1;
c1.printCom();
//前置-- 成员函数
--c1;
c1.printCom();
//c1.operator--()
return 0;
}
需要注意的就是双元操作符返回的是匿名对象,疑问有对象去接,但是单元操作符返回的是引用,也就是要返回对象本身, 因为好多时候是不需要对象去接的,如果返回匿名对象,没人接会直接收走的. 并且成员函数中的单元运算符中是不需要形参的,但是编译器会默认给个this指针过去. 于是我们就得用this指针去操作,前置是先加减再返回,这个还是简单的,那么我们进一步延伸:
class Complex
{
public:
friend Complex operator++(Complex &c1, int);
Complex(int a, int b)
{
this->a = a;
this->b = b;
}
void printCom()
{
cout<<a<<" + "<<b<<"i "<<endl;
}
//通过类成员函数完成后置--
Complex operator--(int)
{
Complex tmp = *this;
this->a--;
this->b--;
return tmp;
}
private:
int a;
int b;
};
//后置++ 全局函数
Complex operator++(Complex &c1, int)
{
Complex tmp = c1;
c1.a++;
c1.b++;
return tmp;
}
int main()
{
Complex c1(1, 2), c2(3, 4);
//后置++ 全局函数
c1++; //operator++(c1);
c1.printCom();
//后置-- 类成员函数
c1--; //c1.operator--()
c1.printCom();
return 0;
}
这里有几点需要说明,后置操作和前置操作的函数名是一样的,但是他们的返回值是不一样的,那这就不属于是函数重载,那么这样写,编译器就会报错,那么怎么样能让编译器区分出是后置还是前置呢?C++创造者们就用占位符在形参中表示这样的方法就是后置操作符. 然后因为后置的原则是先执行后加减, 那么我就定义一个临时对象来保存这个当前的状态(再回顾一点,当进行引用传参的时候,并不是执行拷贝构造,而是常指针的拷贝,和用对象记性另一个对象是不一样的,后者就是执行了拷贝构造函数,如果没有写拷贝构造函数的话,就是浅拷贝),然后加减后我就以匿名对象的身份返回这个临时对象,如果有人接,那么我这个临时对象(因为之前进行了浅拷贝,所以tmp是保持原来的值不变的)就直接给了那个对象,但是如果没人接的话这个临时对象就消失,那么就使用加减后的值了. 这和Java/C中的++和–的运算是一样的.
接下类我们再进行操作符重载的拓展:
我们了解了前置后置等这些操作符后我们来进行c++中特有的”<<”输入输出流进行操作符重写:
class Complex
{
public:
friend ostream& operator<<(ostream &out, Complex &c1);
Complex(int a, int b)
{
this->a = a;
this->b = b;
}
void printCom()
{
cout<<a<<" + "<<b<<"i "<<endl;
}
private:
int a;
int b;
};
ostream& operator<<(ostream &out, Complex &c1)
{
out<<c1.a<<" + "<<c1.b<<"i "<<endl;
return out;
}
int main()
{
Complex c1(1, 2), c2(3, 4);
int a = 10;
char *p = "addddd";
cout<<"a"<<a<<endl;
cout<<"p"<<p<<endl;
//Complex自定义类型
cout<<c1;
//全局函数
//cout<<c1;
//operator<<(cout, c1);
//2 支持链式编程
cout<<c1<<"abcc";
//函数返回值当左值,要求返回一个引用。。。。。
//cout.operator<<(c1).operator<<("abcd");
//s<<"abcd"
//s cout.operator<<(c1);
//s.operator<<("abcd");
return 0;
}
当我们执行cout<<c1;
的时候,调用者是cout,那么cout是哪个类呢?
我们会发现是ostream
这个类,但是我们并没有这个类的源码,所以并不能写成成员函数的形式,所以说像重载输入输出流一般情况下都会写在全局函数上,其他的操作符一般都会写为成员函数. 然后我们为了支持链式编程,所以必须返回对象本身才能当左值.
到这里我们就将操作符重载搞完了,当然这里有Array.cpp, Array.hpp和ArrayTest.cpp三个小例子综合性很不错,可以参考.
const延伸
想必大家见过int A() const{}
这种方式的成员函数吧?这个const是什么鬼呢?我们这里就帮您解释这的本质.
class Test
{
public:
protected:
private:
int a;
const int A()
{
a++;
//b++;
return a;
}
void A(int val)
{
a = val;
}
int BBB()
{
return a;
}
int b;
};
int main()
{
Test t1;
return 0;
}
观点1:const是修饰a,但是通过测试,我们发现,b++也不能编译通过, 这说明:const把a 和 b都修饰了, 所以我们得出const是修饰this, 但是因为this作为函数的第一个参数,被隐藏, const没地方放,就放在了外面. 这就相当于是const Test *pthis
, 因为const修饰的this指针指向的内存空间而不是常指针.
联系方式: reyren179@gmail.com