C++基本知识
1. 声明文件与定义文件编写规范
#ifndef __NAME__
#define __NAME__
......
#endif
-
一个.h文件需要对应至少一个.cpp文件。
-
.cpp定义(define)与.h声明(declare)分开,这是一种C++编程范式。注意这种范式需要注意以下这种情况:
//声明 //a.h void f(); int global;
//定义 //a.cpp #include"a.h" //b.cpp #include"a.h"
编译链接之后
/usr/bin/ld: b.o:(.bss+0x0): multiple definition of `global'; a.o:(.bss+0x0): first defined here /usr/bin/ld: /usr/lib/gcc/x86_64-linux-gnu/8/../../../x86_64-linux-gnu/crt1.o: in function `_start':
在a.h文件当中,f()函数是声明,而global变量是定义;因此在编译过程当中并没有问题,问题出在链接过程当中,链接器告诉我们:a.cpp与b.cpp都有global函数,而且都已经被定义出现重名。
要解决这个问题,只需要在a.h当中把global加入extern改成声明
//新声明 //a.h void f(); extern int global;
//定义文件 //a.cpp #include"a.h" int main() { return 0; } //b.cpp #include"a.h"
编译没有出错。
在头文件当中的变量前加入extern使得变量成声明而非定义
2. 构造函数编写规范
2.1 初始化问题
class complex
{
private:
double re,im;
public:
complex (double r=0,double i=0):re(r),im(i) //一般赋初值在这里而最好不要在body里头
{
}
}
2.2 Default value
-
首先,参数有默认值的构造函数以及 无参数构造函数两者不能同时存在,即重载不能这里起作用。当创建一个没有参数的对象时,程序不知道该用哪一个构造函数。
complex(double r=0,double i=0) { } //不能和以下同时存在 complex() { }
-
构造函数或者其他函数参数有默认值则必须从右往左,默认参数不能只出现在中间或者左边
A(int a=0,int b=1,int c)//error! B(int a,int b,int c=1) //right! C(int a,int b=1,int c) //error!
-
有默认值参数的函数只出现在.h文件当中,而在具体实现.cpp文件当中则不能再次出现默认值
//.h文件 int A(int a=1,int b=2,int c=3); //.cpp文件 int A(int a,int b ,int c) { ...... }
-
缺省构造函数并不是指编译器内部的构造函数,而是需要我们给出没有参数的构造函数;因此,我们应该显式给出无参构造。
-
对于继承问题中,子类继承父类所有函数包括构造函数。当父类只给出有参构造,而子类要进行无参构造。则在子类的构造函数当中必须显式调用父类的构造函数。
class A { private: int age; public: A(int a):age(a) { std::cout<<"start"<<std::endl; } }; class B:public A { B():A(10) //显式调用A的构造函数。 { } };
3. 参数传递与返回值
3.1 非写入函数const必要性分析
class complex
{
private:
double re,im;
public:
complex (double r=0,double i=0):re(r),im(i) //一般赋初值在这里而最好不要在body里头
{
}
double real()const{return re;}//1
double img()const{return im;} //2
}
/*
在1,2 函数处我们在非写入函数前加入const,用来声明该函数不会修改数据防止。如果不加的话,可能程序不会出错但是编译器会出错。如出现以下这种情况
*/
{
const complex c1(1,2);
c1.real();
c2.img();
}
//在声明complex对象的时候,声明为const表明我的初始化不能改变,如果1,2没有加const则表明1,2可能会改变;使得编译器会报错。
3.2 参数传递
参数传递分为传递值与传递引用。由于传递值受值本身大小的影响,传递速度会很慢;因此推荐传递引用。但是如果不希望函数更改数据,可以在最前面加上const,如下所述:
complex& operator +=(cosnt complex&);
3.3 返回值传递
尽量传递引用(前提是可以传递引用的情况下)
如果要求返回一个引用,则不能传递函数体中创建的变量,一般传递的是传进来的参数。
3.4 友元函数
可以直接访问对象数据。
对于以下情况
class complex
{
public:
complex(double r=0,double i=0):re(r),im(i)
{
int func(const complex& param){return param.re+param.im;} //*
}
private:
double re,im
}
//之所以func函数能够直接通过.来访问变量,是因为:相同class的各个objects互为友元。
4. 成员变量与成员函数
4.1 成员变量存在的空间
//a.h
class A{
private:
int i;
public:
void f();
}
//a.cpp
#include"a.h"
void A::f()
{
i++;
}
int main()
{
A a;
a.f();
}
在类A的声明当中我们定义了一个私有变量i,这个变量仅仅是声明,在内存当中是不存在的;因此只有当类具有对象时例如对象a此时i才确确实实存在,因此类中的变量存在于各个具体化的对象当中。
4.2 成员函数
成员函数是属于类的,不是属于对象的。因此上述中a.f()函数是属于A这个类的而非对象a。
// 在C++中
a.f();
//可以在C语言当中被翻译成:
A::f(&a);
//即,将a的地址传给f函数,让它替我执行操作。
因此在成员函数当中可以使用this指针代替对象,来直接引用变量即:
//在类A当中,可以这样写
class A{
private:
int i;
public:
void f();
}
void A::f()
{
this.i=10;//代替i=10;
}
4.3 成员变量的初始化
成员变量最好在initialization中初始化,而不要在构造函数中进行初始化
class A{
private:
int i;
A(int s):i(s) //在这进行初始化
{
//而最好不要在这进行初始化
}
}
5. 动态内存分配:new、delete
5.1 new申请空间并返回空间地址
new int ;
new int [10];
new 对象;
int *p=new int [10];
5.2 delete
delete [] p;
delete a;
tips:
两者是配套使用的,即用new申请的空间,必须使用delete来释放
new申请空间并不是向操作系统申请,而是在分配给整个项目的空间中进行分配,即在堆中进行分配。
不要用delete释放不是用new申请的空间
delete可以用来释放空指针(即使该指针并不是用new产生的)
6. 访问限制
6.1 public
在该区域的变量,能够被除了自己,别的函数也能访问
6.2 private
只能自己访问(这个自己指的是成员函数),
- 该成员函数指的是在class之中的函数例如:
class A{ private: int a; public: void set_a(int i); //成员函数 } int index() //非成员函数 { }
- 同一个类的不同对象可以互相访问对方的私有变量
class{ private: int i; public: void set(int a){this.i=a}; void display(A &b){cout<<"i="<<b.i}; } int main() { A a; a.set(10); A b; b.display(a); }
6.3 protected
继承,即只有这个类的子子孙孙才能访问
6.4 friends
使得非成员函数或者结构或者变量也能访问本类的private对象
class A{ private: int a; public: void f(); friend h(); //函数 friend struct X;//结构 }
即friend函数一定不是成员函数
7. 继承
7.1 含义
7.2 继承关系需要注意的地方
-
同名屏蔽
当父类重载了许多函数例如构造函数,子类又给出同名的构造函数或许其他函数,此时父类的所有跟子类同名的函数都会被屏蔽,只留下子类自己的函数
class A{ void print(){}; void print(int a){}; } class B:public A{ void print(); } int main() { B b(); b(10); //此时调用会报错,因为父类的print(int a)被屏蔽。 }
8. const
8.1 基本含义
Person p1("zengwei");
Person *const p=&p1;//指针是const,即p不能执行p++操作,但是对象可以改变
const Person *p=&p1;//对象是const,意思是不能通过p指针去修改对象,言外之意就是如果p1被其他指针修改了也 是可行的。
Person const *p=&p1;//同上
某个变量是const是在编译时刻才有作用的,由编译器保证不修改。在运行时刻是完全不知道的。
int i; | const int ci=3; | |
---|---|---|
int *ip; | ip=&i; | ip=&ci(error!);//由于*ip非const |
const int *cip; | cip=&i; | cip=&ci; |
-
如果在对象前面加上const
const Person person("zengwei");
则该对象里面的变量都不能修改。因此最好不要这么做。
-
为了保证某个函数不能修改变量,我们可以在函数的原型和定义的地方后面加上const
int Person::Getdata() const { ..... }
在函数后面加入const保证该函数不会修改任何变量。且注意,
此处的const相当于const Person *this;即对象在此处是const。因此函数可改写为:
int Person::Getdata(const Person* this ) { ...... }
基于此,以下重载是正确的
class Person { void f(); //==void f(Person *this); void f()const;//==void f(const Person *this); }
因为参数表不同。
-
如果成员变量是const,则必须在initialization里进行初始化
class Person{ private : const int i; public: Person()i(0){....}; }
-
如果成员变量是const,则不能用该变量用作数组大小。
class Person{ private: const int i; int array[i];//error! public: }
9. 引用
引用变量是一个别名,也就是说,它是某个已存在变量的另一个名字。一旦把引用初始化为某个变量,就可以使用该引用名称或变量名称来指向变量。
- 注意点
- 引用创建时必须赋初始值
- 引用之间不能互相赋值
- 引用不可以为空(Null)
- 一旦引用某一对象就不能再将其作为其他对象的引用
- 当函数需要一个返回引用的时候不能返回该函数的临时变量
引用就是指针
-
如果定义类的时候,需要定义一个引用
class A{ int &m_y; }
由于无法在里面直接初始化,那该如何做呢?
在构造里面的initialization里面对引用进行初始化
class A { private: int &m_y; public: A(int b):m_y(b){....} }
因为只有在initialization里面才是初始化,在任何函数体内部都是赋值。(赋值和初始化不同!)
-
如果函数返回一个引用,则该函数可以用来作为左值使用,同时也可以用来作为右值
int& func(int a) { .... return a; } int main() { func(a)=12; //等价于a=12 int get_data=func(a);//等价于get_data=a; }
-
函数的参数如果是一个表达式,在内部会产生一个临时变量来存储计算的表达式。因此以下用法请注意
void func(int &); func(i*3);//该用法是错误的,因为内部产生的临时变量const int temporary=i*3;而func接受一个非 const的变量,因此会报错。 void func(const int &); func(i*3);//该用法正确,因为在声明func的时候明确接受一个const即表达不会修改该参数,可以放心把参数 给我了
10. 向上造型
向上造型是子类与父类之间的动作。
class A
{
private:
int a;
string name;
public:
A();
void func_a();
}
class B:public A
{
private:
int b;
public:
B();
void func_b();
}
int main()
{
A a;
B b;
A aa=b; //向上造型
A &aaa=b; //向上造型
return 0;
}
向上造型的结果是:aa、aaa看到的只是A中的声明的变量,看不到B中的变量
11. 多态
多态与上节讲的向上造型有很大的关系,同时也和虚函数==(virtual)==有很大关系。同时也涉及函数的动态绑定与静态绑定。
所有含有虚函数的类对象,比没有虚函数对象多一些东西。
一般的类对象:
含有虚函数的类对象:
含有虚函数对象当中会有一部分空间用来存储vptr,它是一个指针,指向虚函数表vtable
假设有类A
class A
{
private:
int i;
public:
virtual func_1();
virtual func_2();
}
class B:public A
{
private:
int j;
public:
virtual func_1()
{
}
virtual func_2()
{
}
virtual func_3();
}
int main()
{
B b;
}
则对应的结构为:
访问虚函数的流程是:
找到vtable 然后直接寻址找到对应的虚函数。
- 基于以上认识,只有在用指针进行引用虚函数的时候,才能保证虚函数是动态绑定即:
int main()
{
A a;
B b;
A* p=(int *)&a;
a=b;
a.func_1();
}
//在a.func_1()此处执行的仍然是a的func_1();因为a=b只是赋值并没有改变地址。
int main()
{
A a;
B b;
A* p=&a;
int* r=(int *)&a;
int* c=(int *)&b;
*r=*c;
p->func_1();
}
//p->func_1()执行的是b的func_1(),因为地址改变了
-
同时也是基于以上认识,当某个类可能被继承时,我们应该把这个类的析构函数设置成虚函数(即动态绑定)
class A { private: int a; public: virtual A(); } class B:public A{ } int main() { A *p=new B(); .... delete p; //如果基类构造函数是虚函数,则在delete p的时候会动态绑定B的析构函数,而非A类的。 }
-
如果子类overriding 父类一个虚函数(overriding 只能谈虚函数其他函数不能被override),且该函数需要在子类同名的函数当中用到,则在调用的时候需要加上父类的作用域:
class A { private: int a; public: virtual A(); void func(); } class B:public A{ void func(){ A::func(); // } }
-
如果父类对对某个虚函数进行的重载,则子类覆盖了该虚函数,则子类必须同时覆盖他的重载函数
class A { private: int a; public: virtual A(); virtual void func_1(); virtual void func_1(int a); } class B:public A{ public: void func_1()override; void func_1(int a)override; }
12. 拷贝构造(复制构造)
区分初始化与赋值运算
A a=aa; //定义时给出值===》初始化 a=aa; //已经定义好了,之后再赋值==》赋值运算 //初始化只能做一次,赋值可以做无数次
拷贝构造是一种初始化,拷贝构造函数与构造函数一样,如果没有显式给出,系统会补上。
12.1 为什么需要拷贝构造函数
当出现以下情况:
class A
{
private :
int index;
public:
A(){cout<<"no para"<<endl;}
A(int a):index(a){cout<<"para"<<endl;}
}
A f(A a){cout<<"A:print"<<endl;return a}
//第一种情况:
int main()
{
A a;
A aa=10;//*
}
//第二种情况:
int main()
{
A a;
A aa=f(a);//**
}
*处是 将10赋值给对象aa,这是正确的做法,因为对于类A存在有参构造,实现方式是直接把10赋值给index。
//实现代码 #include<iostream> static int count=0; class A { private: int index; public: A(){ count++; std::cout<<"count:"<<count<<" no paramtype"<<std::endl;} A(int a):index(a){ count++; std::cout<<"count:"<<count<<" paratype"<<std::endl; } ~A(){ count--; std::cout<<"count:"<<count<<" destructure...."<<std::endl; } }; A func(A a) { std::cout<<"func....."<<std::endl; return a; } int main() { A a=10; return 0; }
**处由于不存在带有对象参数的显示构造函数,因此会用默认的构造函数进行构造,但是析构的时候依然使用显式析构函数。但会出现异常。例子:
#include<iostream> static int count=0; class A { private: int index; public: A(){ count++; std::cout<<"count:"<<count<<" no paramtype"<<std::endl;} A(int a):index(a){ count++; std::cout<<"count:"<<count<<" paratype"<<std::endl; } ~A(){ count--; std::cout<<"count:"<<count<<" destructure...."<<std::endl; } }; A func(A c) { std::cout<<"func....."<<std::endl; return a; } int main() { A a(10); A b=func(a); return 0; } //输出: count:1 paratype func..... count:0 destructure.... count:-1 destructure.... count:-2 destructure.... //说明还有两个临时对象在func函数里面创建了,使用的是默认构造函数。 //首先b的构造函数肯定是默认构造函数,其次在函数体func参数中也使用默认构造函数构造临时对象a。
基于上述情况,我们需要显式给出拷贝构造函数。这样做才是安全的。
T::T(const T&){
.....
}
//下面讨论...里面应该写什么东西。
12.2 如何实现拷贝构造函数
试想:假如对象有指针,如果使用默认构造函数的话,两个对象的指针指向的是同一个地方,当其中一个对象释放指针的时候另外一个对象并不准备释放,结果就会出错。因此,在显式构造函数里面我们必须考虑这种情况。这也是为什么需要给出显式构造函数。
因此,我们应该在拷贝构造函数里面重新申请一个空间供新对象使用
class A
{
private:
char* name; //内部使用指针实现的
public:
A();
A(const A&);
}
A::A(const A& w)
{
name=new char[std::strlen(w.name)+1]; //+1 指的是结束符:\0
std::strcpy(name,w.name);
}
12.3 何时会调用拷贝构造函数
- 当以类本身(指本身不是引用也不是指针)为参数的函数,如12.1节所示
- 当返回的是对象本身的时候
阶段总结
当我们写完一个类,我们必须手动加上几个东西:
-
默认构造函数(无参构造)
-
虚析构函数
-
拷贝构造函数
如果不希望对象被拷贝,可以将拷贝构造函数设置成private,但是设置成private,会使得不能用该对象构造其他对象,同时也不能进行其他操作,因此不建议这么做。
13.析构函数
-
作用
-
调用时机
14. static
-
static在C++中有两层意思:
- 被隐藏了,不被其他文件所看见(相对于全局变量)
- 永久存储(与运行时间一致)
-
在C++中,如果某个类存在静态的成员变量,则该成员变量对所有对象是可见的,而且是唯一的。但是如果要在类对象当中加入静态成员变量,我们需要在类外进行定义。因为在class里面的东西都是声明
class A { private: static int i; //静态成员变量 public: A(); void print(); } int A::i //在此处进行定义 int main() { }
-
对静态成员变量初始化需要在定义的地方,而在构造函数的initialization不能对其进行初始化,但是可以在构造函数内部进行初始化。
-
this指针可以访问静态成员变量。即:this->i
-
-
static也可以用来修饰函数,叫做静态成员函数。对于静态成员函数的访问
class A { private: static int i; public: A(); static void print(){cout<<i;} } int main() { A a; a.print(); //第一种方式 A::print(); //第二种方式 return 0; }
静态的成员函数只能够访问静态的成员变量
静态的成员函数体内部不能出现this,包括this->静态成员变量。该条规则保证在没有对象的情况下依然能够使用。
15. 拷贝赋值
假设两个对象原本都有数据,当我们需要将其中一个对象的内容赋值给另外一个对象的时候,我们就需要拷贝赋值函数。
-
拷贝赋值的过程
假设对象A需要赋值给对象B;首先,我们需要将B内容清空(空间也没了),然后重新申请内存,最后将A的内容赋值给B。总结的代码如下:
class A or B { private: m_data; ..... public: //拷贝赋值函数 String& String::operator=(const String& str) { if(this==&str)return *this //检查是否是自我赋值,很必要!!! delete[] m_data; m_data=new char[strlen(str.m_data)+1]; strcpy(m_data,str.m_data); return *this } } //示例 int main() { class A,B; B=A; return 0 } //注:上面的检查是否自我赋值必要性如下:假设对象A,B的指针m_data指向相同内存,在B进行到delete[]m_data时,那块内存被释放了,接下来的赋值操作就进行不下去了。