C/C++教程

C++总结——语法篇

本文主要是介绍C++总结——语法篇,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

    • Static关键字
        • 1、静态全局变量
        • 2、静态局部变量
        • 3、静态函数
        • 4、静态数据成员
        • 5、静态成员函数
    • 引用
        • 引用与指针的区别
      • 左值、右值、左值引用、右值引用
    • new delete与malloc free

Static关键字

1、静态全局变量

static修饰全局变量使变量成为静态全局变量。该变量存储在静态存储区,只能在本文件中使用,因此其他文件还可以定义名字相同的变量,不会发生冲突。

2、静态局部变量

是指static修饰局部变量。作用域仍然是局部作用域,存放在内存的全局数据区,直至程序运行结束前都不会消失,但只在定义它的函数中可见,只初始化一次。未赋初值时会自动为0。

3、静态函数

指static修饰函数时,该函数变为静态函数。只能在声明他的文件中可见,不能被其他文件使用。其它文件中可以定义相同名字的函数,不会发生冲突。

4、静态数据成员

static修饰类的数据成员。特点:

  1. 对于普通数据成员,每个类的实例都会有一份自己的拷贝。而对于静态数据成员来说,它不属于任何类的对象,无论类产生了多少对象,静态数据成员在程序中也只有一份,由该类的所有对象共享访问,即所有对象操作同一个静态数据成员。
  2. 静态数据成员定义时要分配空间,所以不能在类声明中定义。静态数据成员必须在类内声明,类外初始化。由于静态数据成员不属于任何一个对象,只属于类,所以对静态数据成员的所有操作(除声明外)都要使用类名作为作用域进行操作。并且静态数据成员和普通数据成员一样遵从public,protected,private访问规则。
  3. 静态数据成员存储在全局数据区,但静态成员变量不占用类的大小,在编译时创建并进行初始化

5、静态成员函数

  • 指static修饰类的成员函数。

  • 同静态数据成员一样,静态成员函数不属于任何一个对象,它为整个类服务,所有该类对象共享同一个函数。因此与普通的成员函数不同。由于普通的成员函数是属于类的某一个具体的对象,因此普通成员函数会有一个隐藏的this指针来操作该对象拥有的资源。但静态成员函数不属于任何一个对象,因此,静态成员函数没有this指针,也就不能访问属于类对象的任何的非静态成员函数和非静态数据成员。即:静态成员函数只能访问静态成员函数和静态数据成员。使用方法如下:
    1、使用对象访问
    2、使用类名访问。静态成员函数与静态数据成员一样,必须在类内声明,类外定义,并且定义时不能加static关键字。

  • 使用静态成员函数的一个原因就是可以用它在建立任何对象之前处理静态数据成员这是普通成员函数不能实现的。

  • 静态成员函数不能被申明为const,因为static成员不是任何对象的组成部分

  • 静态成员函数不能被申明为虚函数

引用

  • 引用必须初始化,并且引用一经定义之后就不能改变指向,引用必须引用合法的内存空间。
    但const int& ref = 10 可以被编译通过,因为编译器做了解释: int temp = 10; const int &ref = temp;
    指针与引用的区别
  • 引用的本质:指针常量(指向不可修改,内容可以修改)

引用与指针的区别

  • 指针是实体,引用是某块内存的别名。
  • 自增运算符意义不同:指针++是指地址自增,引用++是指值自增。
  • 指针可以改变指向,而引用只能在定义时被初始化一次,之后不可变
  • 引用不能为空,指针可以为空
  • sizeof(引用)得到的是所指向的变量的大小,而sizeof(指针)得到的是指针本身的大小,一般占4字节。
  • 引用不能与const连用(即没有&const),但指针可以(即有*const)
  • 指针可以有多级指针,而引用只有一级
  • 尽量不要返回函数内部new分配的内存的引用,容易造成内存泄露:如下面例子:
string& foo() {
    string* ptr = new string("123");
    return *ptr;
}
string temp = foo();  //new生成的这块内存无法释放 
//上面这句话可以如下处理,但会麻烦
string& tmp = foo();
string str = tmp;
delete &tmp;

左值、右值、左值引用、右值引用

左值:既能够出现在等号左边,也能出现在等号右边的变量。即左值是可寻址的变量,有持久性,能被修改的变量
右值:只能出现在等号右边的变量。即右值一般是不可寻址的常量,或在表达式求值过程中创建的无名临时对象,短暂性的。右值无法被修改
左值引用:引用一个对象;
右值引用:就是必须绑定到右值的引用,C++11中右值引用可以实现“移动语义”,通过 && 获得右值引用。
右值引用的作用之一:移动语义(std::move),用来减少临时资源的开辟

class Person {
public:
	//默认构造函数
	Person() {
		m_Age = 0;
		m_Height = nullptr;
	}

	//拷贝构造函数
	Person(const Person& p) {
		m_Age = p.m_Age;
		m_Height = new int(*p.m_Height);  //深拷贝,防止浅拷贝
		//cout << "拷贝构造函数申请的地址为:" << m_Height << endl;
		cout << "调用了拷贝构造函数" << endl;
	}

	//移动构造函数
	Person(Person&& p) noexcept {
		m_Age = p.m_Age;
		m_Height = p.m_Height;
		p.m_Height = nullptr;
		cout << "调用了移动构造函数" << endl;
	}

	//赋值运算符
	Person& operator=(Person& p) {
		m_Age = p.m_Age;
		m_Height = new int(*p.m_Height);
		//cout << "赋值运算符申请的地址为:" << m_Height << endl;
		return *this;
	}
	//移动赋值运算符
	Person& operator=(Person&& p) noexcept {
		//先自我检测,释放自身资源
		if (this != &p) {
			delete m_Height;
		}
		
		m_Age = p.m_Age;			//接管p的资源
		m_Height = p.m_Height;

		p.m_Height = nullptr;      //将p的资源置空
	}

	//有参构造函数
	Person(int age, int height) {
		m_Age = age;
		m_Height = new int(height);
		//cout << "有参构造函数申请的地址为:" << m_Height << endl;
		cout << "调用了有参构造函数" << endl;
	}

	//析构函数
	~Person() {
		if (m_Height != nullptr) {
			delete m_Height;
			m_Height = nullptr;
		}
	}

	int m_Age;
	int* m_Height;
};


int main(){
	//传统的左值引用
    int a = 10;
    int& b = a;  // 定义一个左值引用变量
    b = 20;      // 通过左值引用修改引用内存的值

    //下面一行代码无法通过编译,因为等号右边的数无法取地址
    int &var = 10;  

    //上面一行代码可以改成如下的常引用,理由上面已经说过
    const int& var = 10; 
    
    //但改成常引用就无法修改var的值了,因此需要使用右值引用来解决问题
    //下面这行代码就能编译通过
    int&& var = 10;

    //并且右值引用也能改变值
    var = 1; 

	vector<Person> vec;
	Person p1(20, 160);

	vec.push_back(p1);			//p1会在传入参数时调用赋值构造函数被拷贝一次,之后马上销毁
	vec.push_back(std::move(p1));	//p1会被转换成右值,于是会调用移动构造函数,不会调用拷贝构造,
									//提升效率	
 }

右值引用的作用之二:完美转发
完美转发,它指的是函数模板可以将自己的参数“完美”地转发给内部调用的其它函数。所谓完美,即不仅能准确地转发参数的值,还能保证被转发参数的左、右值属性不变。
看下面这段代码

template<typename T>
void f(T&& x){ 
    cout << ++x; 
}
f(2); // 3

函数f接受一个右值x,但是f(2)这条语句将2传入时,可以对2进行自增。这说明右值引用本身时左值,即x时左值。那么在传参时就会出现问题。看下面这段代码

template<typename T>
void g(T&& x) {
    cout << "右值" << endl;
}

template<typename T>
void g(T& x) {
    cout << "左值" << endl;
}

template<typename T>
void f(T&& x) {
    cout << "f右值引用" << endl;
    g(x);
}
template<typename T>
void f(T& x) {
    cout << "f左值引用" << endl;
    g(x);
}

结果如下:
在这里插入图片描述

一共有三个函数,f函数调用了g函数,g函数的作用是如果传入的参数是左值就输出左值,如果传入的参数是右值就输出右值。但是由于g函数的参数是通过f函数传入的(即x先通过外部的f函数传入,再由f函数传给g),经过第一次传参时x已经变为左值了(可以和第一个例子比对着看),所以g(x)永远只会输出左值(即永远只会和第二个函数模板匹配),这明显与我们想要的结果不匹配。那么我们可以这么写:

template<typename T>
void f(T&& x) {
    cout << "f右值引用" << endl;
    g(std::forward<T>(x));
}
template<typename T>
void f(T& x) {
    cout << "f左值引用" << endl;
    g(std::forward<T>(x));
}

此时,输出的结果如下:
在这里插入图片描述
符合我们的预期。std::forward函数保持了 x 的引用类型。
那么再结合移动语义想象一下这样的一个例子:你需要通过函数传入的参数进行一个赋值(或者拷贝操作),但是如果不保持参数的性质,虽然你为了减小开销,外面传入的是一个右值,但是一进入函数就会变成左值。举个例子:
回到之前的移动语义的例子中来,之前的代码不变,我们增加一个函数

void fun(Person&& p) {
	Person p2(p);
}
int main(int argc, char* argv[])
{
	Person p1(20, 160);
	fun(std::move(p1));
}

结果如下:
在这里插入图片描述
我们在主函数中构造了P1对象,fun()函数本义是利用移动构造函数来减小复制次数,但是可以看到尽管我们在main函数中使用了右值,但是fun函数里任然使用了拷贝构造函数。我们做如下修改:

Person p2(std::forward<Person>(p));

结果如下:
在这里插入图片描述
可以看到符合我们的预期

new delete与malloc free

相同点:都能从堆上申请、释放空间
区别:

  1. new与delete属于运算符,而malloc是函数
  2. malloc只负责开辟空间,new不仅仅有malloc的功能,可以进行数据的初始化
  3. malloc开辟内存失败返回nullptr指针;new抛出的是bad_alloc类型的异常
  4. free只会释放空间,而delete会先调用类的析构函数,在释放空间。
这篇关于C++总结——语法篇的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!