C++基础知识

基础知识点

C++学习网站:
English:http://en.cppreference.com/w/
中 文 :http://zh.cppreference.com/w/首页
http://www.runoob.com/cplusplus/cpp-tutorial.html

1.类的声明

类名 对象名
例如:
Clock myclock;
注意:对象占据的内存只存放数据成员,每个函数在内存中只占据一份空间。

2. 模板类

a.模板的声明

template<class T>    
class A{
       public: T a; 
       T b; 
       T hy(T c, T &d);
       void h();
    
};

b.类模板外部定义成员函数的方法

template<模板形参列表>   
函数返回类型 类名<模板形参名>::函数名(参数列表){函数体},  

比如有两个模板形参T1,T2的类A中含有一个voidh()函数,则定义该函数的语法为:

template<class T1,class T2>
void A<T1,T2>::h(){
    
}

c.类模板对象的创建

比如一个模板类A,则使用类模板创建对象的方法为A< int > m;

3.继承

派生类不继承基类的构造函数与析构函数。

(1). 公有继承

基类的public和protected 的访问属性在派生类中不变,而基类的private不可访问

(2). 私有继承

基类的public和protected的访问属性在派生类中变成private属性,而基类的private不可访问

(3). 保护继承

基类的public和protected的访问属性在派生类中变成rotected属性,而基类的private不可访问(protected可以被派生类访问,但不能被对象访问)

(4). 类型兼容

通过以下方式调用基类函数(适用于继承多个基类,与多态正相反):
基类 p=派生类
p.fun();
或 基类 *p=派生类
p->fun();

class base1{
	public:
	void display()const{cout<<"base1::diaplay"<<endl;}
};
class base2:public base1{
	public:
	void display()const{cout<<"base2::diaplay"<<endl;}
};
class derived:public base2{
	public:
	void display()const{cout<<"base3::diaplay"<<endl;}
};
void fun(base1 * base)
{
	base->display();
};
int main ()
{
   base1 fun1;
   base2 fun2;
   derived fun3;
   fun(&fun1);   //输出base1::diaplay
   fun(&fun2);   //输出base1::diaplay
   fun(&fun3);   //输出base1::diaplay
 
   return 0;

(5). 派生类的构造函数

派生类构造函数执行顺序:

  1. 调用基类构造函数,调用顺序按照他们被继承时申明的顺序(从左向右),调用的方式按照派生类构造函数初始化列表确定(列表无基类构造函数声明则调用基类默认构造函数,有的话根据参数调用对应的构造函数)
  2. 对派生类新增的成员对象初始化,调用顺序按照它们在类中的声明顺序
  3. 执行派生类的构造函数体中的内容
a. 基类最好写默认构造函数,不然派生类调用默认构造函数的时候回找不到基类的构造函数导致编译失败。
b. 如果继承方式是 A -> B -> C

则派生构造的调用方式是 A -> B -> C

class base1{
	public:
	base1(){cout<<"base1::inital"<<endl;}
	base1(int i){cout<<"base1::diaplay"<<i<<endl;}
	base1(char i){cout<<"base1::char"<<i<<endl;}
};
class base2{
	public:
	base2(){cout<<"base2::diaplay"<<endl;}
};
class base3{
	public:
	base3(int m){cout<<"base3::diaplay"<<m<<endl;}
	base3(){cout<<"base3::inital"<<endl;}
};
//类的声明确定基类构造函数的执行顺序
class derived:public base3,public base1,public base2{
	public:
//派生类构造函数的基类声明确定上面的调用基类构造函数的类别,无声明则调用默认
    derived(char i,int j,int n,int k):base1(i),menber3(k),menber1(n),base3(j){};
	base1 menber1;  //派生类成员依次初始化
	base2 menber2;
	base3 menber3;
};

int main ()
{
   derived  obj(1,2,3,4);
   return 0;
}
//输出结果:
base3::display2
base1::char?
base2::display
base1::display3
base2::display
base3::display4

(6) 派生类的析构函数

调用方式与构造函数正好相反:
派生类析构函数体-> 派生类对象成员所在类的析构函数 -> 基类析构函数

(7)派生作用域

(1) 如果派生类含有与基类相同的变量和函数,则会隐藏基类的变量与函数,如果需要调用基类的成员则需加作用域,如下:

graph TD
base0-->base1
base0-->base2
base1-->derived
base2-->derived
derived.base1::val=2;
derived.base1::fun();

(2)如果base0含有独一无二的成员。

调用方法1:添加作用域

derived.base1::val=2;
derived.base1::fun();

调用方法2:使用虚基类

class base1:virtual public base0{
    xxx
}
class base2:virtual public base0{
    xxx
}
derived.val=2;
derived.fun();

(8)组合与继承

  • 组合是“有一个”(has-a)的关系
    解释:整体与一部分的关系,好比汽车与零件
  • 继承是“是一个”(is-a)的关系
    解释:特殊与一般的关系,好比汽车、卡车等与车的关系

4. 多态性

1. 多态的类型

  • 重载多态
  • 强制多态
  • 包含多态
  • 参数多态

2. 多态的实现

  • 编译时的多态
  • 运行时的多态

3. 不能重载的运算符

  • 类属关系运算符“.”
  • 成员指针运算符“.*”
  • 作用域分辨符“::”
  • 三目运算符“?:”

4. 使用方式 (一个基类,多个派生类,与继承相反)

多态与非多态的实质区别就是函数地址是早绑定还是晚绑定。如果函数的调用,在编译器编译期间就可以确定函数的调用地址,并生产代码,是静态的,就是说地址是早绑定的。而如果函数调用的地址不能在编译器期间确定,需要在运行时才确定,这就属于晚绑定。
  那么多态的作用是什么呢,封装可以使得代码模块化,继承可以扩展已存在的代码,他们的目的都是为了代码重用。而多态的目的则是为了接口重用。也就是说,不论传递过来的究竟是那个类的对象,函数都能够通过同一个接口调用到适应各自对象的实现方法。

基类 virtual void display();
派生类1 void display();
派生类2 void display();

base *p=派生类x
p->display();//显示方式根据派生类x而定

5. 纯虚函

实例:virtual void display() = 0;

6. Lamda表达式

基本语法如下

[capture](parameters) mutable ->return-type{statement}

各部分解释如下

1.[capture]:捕捉列表。捕捉列表总是出现在Lambda函数的开始处。实际上,[]是Lambda引出符。编译器根据该引出符判断接下来的代码是否是Lambda函数。捕捉列表能够捕捉上下文中的变量以供Lambda函数使用;
2.(parameters):参数列表。与普通函数的参数列表一致。如果不需要参数传递,则可以连同括号“()”一起省略;
3.mutable:mutable修饰符。默认情况下,Lambda函数总是一个const函数,mutable可以取消其常量性。在使用该修饰符时,参数列表不可省略(即使参数为空);
4.->return-type:返回类型。用追踪返回类型形式声明函数的返回类型。我们可以在不需要返回值的时候也可以连同符号->一起省略。此外,在返回类型明确的情况下,也可以省略该部分,让编译器对返回类型进行推导;
5.{statement}:函数体。内容与普通函数一样,不过除了可以使用参数之外,还可以使用所有捕获的变量。
注意到,lamda表达式,与普通函数,最大的不同,在于拥有捕获列表。捕获列表有一下几种形式

[var] 表示值传递方式捕捉变量var;
[=] 表示值传递方式捕捉所有父作用域的变量(包括this);
[&var] 表示引用传递捕捉变量var;
[&] 表示引用传递方式捕捉所有父作用域的变量(包括this);
[this] 表示值传递方式捕捉当前的this指针。
注意的是,捕获列表可以组合,但是不能重复。

// 合法的例子:
[&, a, b]
[=, &a, &b]
// 非法的例子:
[=, a]
[&, &a]

lamda的使用
用于函数参数
在使用STL时,我们有时需要传递一些函数参数给STL的算法函数,在没有lamda表达式之前,我们有两种做法,一种是传递函数,一种是传递函数对象。现在,我们可以简单的传递lamda表达式,比如:

for_each(vec.begin(), vec.end(), [](int v){cout << v < endl;})

7.浅拷贝与深拷贝

       当出现类的等号赋值时,会调用拷贝函数,在未定义显示拷贝构造函数的情况下,系统会调用默认的拷贝函数——即浅拷贝,它能够完成成员的一一复制。当数据成员中没有指针时,浅拷贝是可行的。
       但当数据成员中有指针时 ,如果采用简单的浅拷贝,则两类中的两个指针将指向同一个地址,当对象快结束时,会调用两次析构函数,而导致指针悬挂现象。所以,这时,必须采用深拷贝。
       深拷贝与浅拷贝的区别就在于深拷贝会在堆内存中另外申请空间来储存数据,从而也就解决了指针悬挂的问题。指向不同的内存空间,但内容是一样的
       简而言之,当数据成员中有指针时,必须要用深拷贝。
这里再总结一下深复制和浅复制的具体区别:

       当拷贝对象状态中包含其他对象的引用时,如果需要复制的是引用对象指向的内容,而不是引用内存地址,则是深复制,否则是浅复制。
       浅复制就是成员数据之间的赋值,当值拷贝时,两个对象就有共同的资源。而深拷贝是先将资源复制一份,是对象拥有不同的资源(内存区域),但资源内容(内存里面的数据)是相同的。
       与浅复制不同,深复制在处理引用时,如果改变新对象内容将不会影响到原对象内容
       与深复制不同,浅复制资源后释放资源时可能会产生资源归属不清楚的情况(含指针时,释放一方的资源,其实另一方的资源也随之释放了),从而导致程序运行出错
       深复制和浅复制还有个区别就是执行的时候,浅复制是直接复制内存地址的,而深复制需要重新开辟同样大小的内存区域,然后复制整个资源。
       有了前面的铺垫,下面开始讲讲拷贝构造函数和赋值函数,其实前面第一部分也已经介绍了许多

      这里以string 类为例来进行说明。

class String
{
public:
    String(const char *str = NULL);
    String(const String &rhs);
    String& operator=(const String &rhs);
    ~String(void){
        delete[] m_data;
    }

private:
    char *m_data;
};

//构造函数
String::String(const char* str)
{
    if (NULL == str)
    {
        m_data = new char[1];
        *m_data = '\0';
    }
    else
    {
        m_data = new char[strlen(str) + 1];
        strcpy(m_data, str);
    }
}

//拷贝构造函数,无需检验参数的有效性
String::String(const String &rhs)
{
    m_data = new char[strlen(rhs.m_data) + 1];
    strcpy(m_data, rhs.m_data);
}

//赋值函数
String& String::operator=(const String &rhs)
{
    if (this == &rhs)
        return *this;

    delete[] m_data; m_data = NULL;
    m_data = new char[strlen(rhs.m_data) + 1];
    strcpy(m_data, rhs.m_data);

    return *this;
}

类String 拷贝构造函数与普通构造函数的区别是:在函数入口处无需与 NULL 进行比较,这是因为“引用”不可能是NULL,而“指针”可以为NULL。(这是引用与指针的一个重要区别)。然后需要注意的就是深复制了。
相比而言,对于类String 的赋值函数则要复杂的多:

1、首先需要执行检查自赋值

这是防止自复制以及间接复制,如 b = a; c = b; a = c;之类,如果不进行自检的话,那么后面的 delete 将会进行自杀操作,后面随之的拷贝操作也会出错,所以这是关键的一步。还需要注意的是,自检是检查地址,而不是内容,内存地址是唯一的。必须是 if(this == &rhs)

2、释放原有的内存资源

必须要用 delete 释放掉原有的内存资源,如果此时不释放,该变量指向的内存地址将不再是原有内存地址,也就无法进行内存释放,造成内存泄露。

3、分配新的内存资源,并复制资源

这样变量指向的内存地址变了,但是里面的资源是一样的

4、返回本对象的引用

这样的目的是为了实现像 a = b = c; 这样的链式表达,注意返回的是 *this 。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值