一、左值和右值

1.左值 [可以取地址的对象就是左值]

左值是一个表示数据的表达式,比如:变量名、解引用的指针变量。一般地,我们可以获取它的地址和对它赋值,但被 const 修饰后的左值,不能给它赋值,但是仍然可以取它的地址。总体而言,可以取地址的对象就是左值。

// 以下的a、p、*p、b都是左值
int a = 3;
int* p = &a;
*p;
const int b = 2;

2.右值[不可以取地址的对象就是右值]

右值也是一个表示数据的表达式,比如:字面常量、表达式返回值,传值返回函数的返回值(是传值返回,而非传引用返回),右值不能出现在赋值符号的左边且不能取地址。总体而言,不可以取地址的对象就是右值。

double x = 1.3, y = 3.8;
// 以下几个都是常见的右值
10; // 字面常量
x + y; // 表达式返回值
fmin(x, y); // 传值返回函数的返回值
以下写法均不能通过编译:

10 = 4;、x + y = 4;、fmin(x, y) = 4;,VS2015 编译报错:error C2106: “=”: 左操作数必须为左值。原因:右值不能出现在赋值符号的左边。
&10;、&(x + y);、&fmin(x, y);,VS2015 编译报错:error C2102: “&” 要求左值。原因:右值不能取地址。

3.总结

区分左值和右值,终究还是要看能否取地址。

二、左值引用和右值引用

传统的 C++ 语法中就存在引用语法,而 C++11标准中新增了右值引用的语法特性,因此为了区分两者,将C++11标准出现之前的引用称为左值引用。

无论左值引用还是右值引用,都是给对象取别名。

1.左值引用

左值引用就是对左值的引用,给左值取别名。

// 以下几个是对上面左值的左值引用
int& ra = a;
int*& rp = p;
int& r = *p;
const int& rb = b;

2.右值引用

右值引用就是对右值的引用,给右值取别名。

右值引用的表示是在具体的变量类型名称后加两个 &,比如:int&& rr = 4;。
// 以下几个是对上面右值的右值引用
int&& rr1 = 10;
double&& rr2 = x + y;
double&& rr3 = fmin(x, y);
注意:
右值引用 引用 右值,会使右值被存储到特定的位置。
也就是说,右值引用变量其实是左值,可以对它取地址和赋值(const右值引用变量可以取地址但不可以赋值,因为 const 在起作用)。
当然,取地址是指取变量空间的地址(右值是不能取地址的)。
比如:
1. double&& rr2 = x + y;
  &rr2;
  rr2 = 9.4;
  右值引用 rr2 引用右值 x + y 后,该表达式的返回值被存储到特定的位置,不能取表达式返回值 x + y 的地址,但是可以取 rr2 的地址,也可以修改 rr2 。 const double&& rr4 = x + y; &rr4; 可以对 rr4 取地址,但不能修改 rr4,即写成rr4 = 5.3;会编译报错。
2. const double&& rr4 = x + y;
  &rr4;
  可以对 rr4 取地址,但不能修改 rr4,即写成rr4 = 5.3;会编译报错

现在我们知道左值引用可以引用左值,右值引用可以引用右值。
那么左值引用是否可以引用右值?右值引用是否可以引用左值呢?
下面的对比与总结给出了答案。

3.对比与总结

3.1 左值引用总结:

左值引用只能引用左值,不能直接引用右值。
但是const左值引用既可以引用左值,也可以引用右值

// 1.左值引用只能引用左值
int t = 8;
int& rt1 = t;
//int& rt2 = 8; // 编译报错,因为8是右值,不能直接引用右值 // 2.但是const左值引用既可以引用左值
const int& rt3 = t; const int& rt4 = 8; // 也可以引用右值
const double& r1 = x + y;
const double& r2 = fmin(x, y);
问:为什么const左值引用也可以引用右值
答:在 C++11标准产生之前,是没有右值引用这个概念的,当时如果想要一个类型既能接收左值也能接收右值的话,需要用const左值引用,比如标准容器的 push_back 接口:void push_back (const T& val)。
也就是说,如果const左值引用不能引用右值的话,有些接口就不好支持了。

下面就是 C++98标准中相关接口const左值引用引用右值的例子:

vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);

3.2 右值引用总结:

右值引用只能引用右值,不能直接引用左值。
但是右值引用可以引用被move的左值

move,本文指std::move(C++11),作用是将一个左值强制转化为右值,以实现移动语义。左值被 move 后变为右值,于是右值引用可以引用。
// 1.右值引用只能引用右值
int&& rr1 = 10;
double&& rr2 = x + y;
const double&& rr3 = x + y; int t = 10;
//int&& rrt = t; // 编译报错,不能直接引用左值 // 2.但是右值引用可以引用被move的左值
int&& rrt = std::move(t);
int*&& rr4 = std::move(p);
int&& rr5 = std::move(*p);
const int&& rr6 = std::move(b);

三、左值引用的使用场景及实际意义

1.左值引用场景

1.1左值引用做参数

当引用作为参数的时候,其效果跟利用指针作为参数的效果相当。当调用函数时,函数中的形参就会被当成实参变量或者对象的一个别名使用,也就是说函数中对形参的各种操作实际上就是对实参本身的操作,而非简单的实参变量或者对象的值拷贝给形参

void func1(string s)
{...} void func2(const string& s)
{...} int main()
{
string s1("Hello World!");
func1(s1); // 由于是传值传参且做的是深拷贝,代价较大
func2(s1); // 左值引用做参数减少了拷贝,提高了效率
//通常函数调用时采用值传递的方式,系统会在内存中开辟空间用来存储形参变量,并将实参变量的值拷贝给形参变量,所以形参变量只是实参变量的副本而已,并且如果函数传递的是类的对象,系统还会调用类中的拷贝构造函数来构造形参对象
//使用引用作为参数传递时,由于此时形参只是传递函数的实参变量或者对象的别名而非副本,故系统不会耗费时间在内存中开辟空间来存储形参,因此如果参数传递的数据较大,建议使用引用作为函数的形参,提高函数的时间效率,节省内存空间
//指针作为函数的形参,虽然达到的效果跟使用引用一样,但当调用函数时仍然需要为形参指针分配空间,引用则不需要【引用在底层也会分配指针大小的空间,在汇编底层角度,引用和指针是一样的,不过引用类似于常量指针】。推荐使用引用而非指针作为函数的传递函数

return 0;
}

1.2.左值引用做返回值(仅限于对象出了函数作用域以后还存在的情况)

string s2("hello");
// string operator+=(char ch) 传值返回存在拷贝且是深拷贝
// string& operator+=(char ch) 左值引用做返回值没有拷贝,提高了效率
s2 += '!';

2.实际意义(减少拷贝,节省内存,提高效率)

传值传参和传值返回都会产生拷贝,有的甚至是深拷贝,代价很大。而左值引用的实际意义在于做参数和做返回值都可以减少拷贝,从而提高效率

3.短板

左值引用虽然较完美地解决了大部分问题,但对于有些问题仍然不能很好地解决。

全局变量

当对象(全局变量)出了函数作用域以后仍然存在时,可以使用左值引用返回,这是没问题的。

string& operator+=(char ch)
{
  push_back(ch);
  return *this;
}

局部变量

但当对象(对象是函数内的局部对象)出了函数作用域以后不存在时,就不可以使用左值引用返回了。

string operator+(const string& s, char ch)
{
string ret(s);
ret.push_back(ch);
return ret;
} // 拿现在这个函数来举例:ret是函数内的局部对象,出了函数作用域后会被析构,即被销毁了
// 若此时再返回它的别名(左值引用),也就是再拿这个对象来用,就会出问题 /*
①:不能返回局部变量的引用。局部变量会在函数返回后被销毁,此时对 局部变量的引用就会成为“无所指”的引用,程序会进入未知状态。
②:不能返回函数内部通过 new 分配的内存的引用。虽然不存在局部变量的被动销毁问题,但如果被返回的函数的引用只是作为一个临时变量出现,而没有将其赋值给一个实际的变量,
那么就可能造成这个引用所指向的空间(有 new 分配)无法释放的情况(由于没有具体的变量名,故无法用 delete 手动释放该内存),从而造成内存泄漏。
*/

于是,对于第二种情形,左值引用也无能为力,只能传值返回。

四、右值引用

于是,为了解决上述传值返回的拷贝问题,C++11标准就增加了右值引用 和 移动语义

4.1 移动语义(Move semantics)

将一个对象中的资源移动到另一个对象(资源控制权的转移)。

4.1.1 移动构造

① 概念:

转移参数右值的资源来构造自己。

// 这是一个模拟string类的实现的移动构造
string(string&& s)
:_str(nullptr)
, _size(0)
, _capacity(0)
{
swap(s);
}

拷贝构造函数和移动构造函数都是构造函数的重载函数,所不同的是:

  • 拷贝构造函数的参数是 const左值引用,接收左值或右值
  • 移动构造函数的参数是右值引用,接收右值或被 move 的左值

注:当传来的参数是右值时,虽然拷贝构造函数可以接收,但是编译器会认为移动构造函数更加匹配,就会调用移动构造函数。

总的来说,如果这两个函数都有在类内定义的话,在构造对象时:

  • 若是左值做参数,那么就会调用拷贝构造函数,做一次拷贝(如果是像 string 这样的在堆空间上存在资源的类,那么每调用一次拷贝构造就会做一次深拷贝)。
  • 若是右值做参数,那么就会调用移动构造,而调用移动构造就会减少拷贝(如果是像 string 这样的在堆空间上存在资源的类,那么每调用一次移动构造就会少做一次深拷贝)。

比如执行下面这几行代码:

string s("Hello World11111111111111111");
string s1 = s; // s是左值,所以调用拷贝构造函数
string s2 = move(s); // s被move后变为右值,所以调用移动构造函数,s的资源会被转移用来构造s2
// 要注意的是,move一般是不这样用的,因为s的资源被转走了

② 移动构造有无的比较

比如执行语句 cout << MyLib::to_string(1234) << endl;

  • 只有拷贝构造没有移动构造:

  • 在 to_string 函数栈帧销毁前,用局部对象 str 拷贝构造出临时对象返回到函数调用处

  • 既有拷贝构造也有移动构造:

  • 在 to_string 函数栈帧销毁前,用局部对象 str (反正 str 要销毁,将 str 视为右值,直接转移 str 的资源 )移动构造出临时对象返回到函数调用处

比如执行语句MyLib::string ret = MyLib::to_string(1234);

  • 只有拷贝构造没有移动构造:

  • 在 to_string 函数栈帧销毁前,先用局部对象 str 拷贝构造出临时对象返回到函数调用处,to_string 函数栈帧销毁后,再用临时对象拷贝构造出 ret 。

         但现在的编译器一般都会进行优化:因为临时对象有 ret 来接收,这样的话临时对象的创建和销毁就显得多余了,不如省略掉这一步,直接用 str 拷贝构造出 ret 。

  • 既有拷贝构造也有移动构造:

  • 在 to_string 函数栈帧销毁前,由于局部对象 str 是左值(可以对它取地址),所以用 str 拷贝构造出临时对象返回到函数调用处,to_string 函数栈帧销毁后,由于临时对象是右值,所以用临时对象移动构造出 ret 。 但现在的编译器一般都会进行优化:因为临时对象有 ret 来接收,先拷贝构造出临时对象再用它移动构造出 ret ,临时对象好像没必要产生一样,不如省略掉。既然 str 是 to_string 函数栈帧的局部对象,最后还是要销毁,不如将 str 视为右值,直接转移 str 的资源用来构造 ret ,也就是直接用 str 移动构造出 ret 。

再比如执行下面的代码:

4.1.2移动赋值

① 概念

  转移参数右值的资源来赋给自己。

// 这是一个模拟string类的实现的移动赋值
string& operator=(string&& s)
{
  swap(s);
  return *this;
}

拷贝赋值函数和移动赋值函数都是赋值运算符重载函数的重载函数,所不同的是:

  • 拷贝赋值函数的参数是 const左值引用,接收左值或右值
  • 移动赋值函数的参数是右值引用,接收右值或被 move 的左值

注:当传来的参数是右值时,虽然拷贝赋值函数可以接收,但是编译器会认为移动赋值函数更加匹配,就会调用移动赋值函数。

总的来说,如果这两个函数都有在类内定义的话,在进行对象的赋值时:

  • 若是左值做参数,那么就会调用拷贝赋值,做一次拷贝(如果是像 string 这样的在堆空间上存在资源的类,那么每调用一次拷贝赋值就会做一次深拷贝)。
  • 若是右值做参数,那么就会调用移动赋值,而调用移动赋值就会减少拷贝(如果是像 string 这样的在堆空间上存在资源的类,那么每调用一次移动赋值就会少做一次深拷贝)。

比如下面这几行代码:

string s("11111111111111111");
string s1("22222222222222222");
s1 = s; // s是左值,所以调用拷贝赋值函数 string s2("333333333333333333");
s2 = std::move(s); // s被move后变为右值,所以调用移动赋值函数,s的资源会被转移用来赋给s2
// 要注意的是,move一般是不这样用的,因为s的资源被转走了

② 移动赋值有无的比较

比如执行下面的语句:
MyLib::string ret("111111111111111111111111");
ret = MyLib::to_string(12345);

  • 没有移动赋值(有移动构造和拷贝赋值)

  • 用 str(编译器视 str 为右值)移动构造出临时对象作为返回值,再用临时对象拷贝赋值给 ret 

  • 有移动赋值:

  • 用 str(编译器视 str 为右值)移动构造出临时对象作为返回值,由于临时对象是右值,再用临时对象移动赋值给 ret 。

4.2 右值引用的使用场景

4.2.1  返回函数的局部变量

即上面的使用场景to_string()返回str局部变量的使用场景,主要是解决函数参数的传递中(针对返回的将亡值,即局部大块数据),解决大块数据或对象的传递效率和空间不如意的问题

QList<Pin*> getModelPins() const
{
QList<Pin*> pins;
for (auto& pin : m_lstPins) {
pins << pin.data();
}
return std::move(pins);
} //-->>调用 QList<Pin*> getInstModelPins(std::string instId) const
{
auto inst = d->getInst(instId);
if (inst) {
auto pins = inst->getModelPins();
return std::move(pins);
}
return QList<Pin*>();
}

4.2.2 C++11标准的STL 容器接口

除了上面的使用场景之外,C++11标准的STL 容器的相关接口函数也增加了右值引用版本。

比如:

3.完美转发(Perfect forwarding)

3.1 引入原因 (保护左值/右值属性)

在此之前我们需要知道什么是万能引用

确定类型的 && 表示右值引用(比如:int&& ,string&&),
函数模板中的 && 不表示右值引用,而是万能引用模板类型必须通过推断才能确定,其接收左值后会被推导为左值引用,接收右值后会被推导为右值引用。

注意区分右值引用和万能引用:下面的函数的 T&& 并不是万能引用,因为 T 的类型在模板实例化时已经确定。

template<typename T>
class A
{
void func(T&& t); // 模板实例化时T的类型已经确定,调用函数时T是一个确定类型,所以这里是右值引用
};

让我们通过下面的程序来认识万能引用:

template<typename T>
void f(T&& t) // 万能引用
{
//...
} int main()
{
int a = 5; // 左值
f(a); // 传参后万能引用被推导为左值引用

const string s("hello"); // const左值
f(s); // 传参后万能引用被推导为const左值引用 f(to_string(1234)); // to_string函数会返回一个string临时对象,是右值,传参后万能引用被推导为右值引用 const double d = 1.1;
f(std::move(d)); // const左值被move后变成const右值,传参后万能引用被推导为const右值引用 return 0;
}

于是我们会用万能引用去做一些有意义的事,比如下面的代码:

void Func(int& x) {    cout << "左值引用" << endl; }

void Func(const int& x) { cout << "const左值引用" << endl; }

void Func(int&& x) { cout << "右值引用" << endl; }

void Func(const int&& x) { cout << "const右值引用" << endl; }

template<typename T>
void f(T&& t) // 万能引用
{
Func(t); // 根据参数t的类型去匹配合适的重载函数
} int main()
{
int a = 4; // 左值
f(a); const int b = 8; // const左值
f(b); f(10); // 10是右值 const int c = 13;
f(std::move(c)); // const左值被move后变成const右值 return 0;
}

运行程序后,我们本以为打印的结果是:

  左值引用
  const左值引用
  右值引用
  const右值引用

但实际的结果却是:

后两行的运行结果跟我们预想的不一样。

那么这是怎么一回事呢?
其实在本文的前面已经讲过了,右值引用变量其实是左值,所以就有了上面的运行结果。

具体解释:

  • f(10);
      10是右值,传参后万能引用被推导为右值引用,但该右值引用变量其实是左值,因此实际调用的函数是void Func(int& x)。
  • f(std::move(c));
      const左值被move后变成const右值,传参后万能引用被推导为const右值引用,但该const右值引用变量其实是const左值,因此实际调用的函数是void Func(const int& x)。

也就是说,右值引用失去了右值的属性

但我们希望的是,在传递过程中能够保持住它的原有的左值或右值属性,于是 C++11标准提出完美转发

3.2 概念

完美转发是指在函数模板中完全依照模板的参数类型将参数传递给当前函数模板中的另外一个函数

因此,为了实现完美转发,除了使用万能引用之外,我们还要用到std::forward(C++11),它在传参的过程中保留对象的原生类型属性

这样右值引用在传递过程中就能够保持右值的属性。

void Func(int& x) { cout << "左值引用" << endl; }

void Func(const int& x) { cout << "const左值引用" << endl; }

void Func(int&& x) { cout << "右值引用" << endl; }

void Func(const int&& x) { cout << "const右值引用" << endl; }

template<typename T>
void PerfectForward(T&& t) // 万能引用
{
Func(std::forward<T>(t)); // 根据参数t的类型去匹配合适的重载函数
} int main()
{
int a = 4; // 左值
PerfectForward(a); const int b = 8; // const左值
PerfectForward(b); PerfectForward(10); // 10是右值 const int c = 13;
PerfectForward(std::move(c)); // const左值被move后变成const右值 return 0;
}

实现完美转发需要用到万能引用和 std::forward 。

3.3 使用场景

除了上面的使用场景之外,C++11标准的 STL 容器的相关接口函数也实现了完美转发,这样就能够真正实现右值引用的价值

比如 STL 库中的容器 list :

上面四个接口函数都调用 _Insert 函数,_Insert 函数模板实现了完美转发

再比如自己模拟实现的 list(这里只写出主要部分):

template<class T>
struct ListNode
{
ListNode* _next = nullptr;
ListNode* _prev = nullptr;
T _data;
}; template<class T>
class List
{
typedef ListNode<T> Node;
public:
List()
{
_head = new Node;
_head->_next = _head;
_head->_prev = _head;
} void PushBack(const T& x) // 左值引用
{
Insert(_head, x);
} void PushFront(const T& x) // 左值引用
{
Insert(_head->_next, x);
} void PushBack(T&& x) // 右值引用
{
Insert(_head, std::forward<T>(x)); // 关键位置:保留对象的原生类型属性
} void PushFront(T&& x) // 右值引用
{
Insert(_head->_next, std::forward<T>(x)); // 关键位置:保留对象的原生类型属性
} template<class TPL> // 该函数模板实现了完美转发
void Insert(Node* pos, TPL&& x) // 万能引用
{
Node* prev = pos->_prev;
Node* newnode = new Node;
newnode->_data = std::forward<TPL>(x); // 关键位置:保留对象的原生类型属性 // prev newnode pos
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = pos;
pos->_prev = newnode;
} private:
Node* _head;
};

只要是右值引用,由当前函数再传递给其它函数调用,要保持右值属性,必须实现完美转发

4.重大意义

右值引用(及其支持的移动语义和完美转发)是 C++11 中加入的最重要的新特性之一,它使得 C++ 程序的运行更加高效。

————————————————
版权声明:本文为CSDN博主「Butayarou」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/m0_59938453/article/details/125858335

详解 C++ 左值、右值、左值引用以及右值引用的更多相关文章

  1. Android组件---四大布局的属性详解

    [声明] 欢迎转载,但请保留文章原始出处→_→ 文章来源:http://www.cnblogs.com/smyhvae/p/4372222.html Android常见布局有下面几种: LinearL ...

  2. 20160129.CCPP体系详解(0008天)

    程序片段(01):函数.c+call.c+测试.cpp 内容概要:函数 ///函数.c #include <stdio.h> #include <stdlib.h> //01. ...

  3. (Dos)/BAT命令入门与高级技巧详解(转)

    目录 第一章 批处理基础 第一节 常用批处理内部命令简介 1.REM 和 :: 2.ECHO 和 @ 3.PAUSE 4.ERRORLEVEL 5.TITLE 6.COLOR 7.mode 配置系统设 ...

  4. Java 运算符-=,+=混合计算详解

    +=与-=运算符混合计算解析: int x = 3; x += x -= x -= x += x -= x; 详解:算数运算按运算符优先级运算,从右至左计算. 1. x=x-x; 实际为 3 - 3 ...

  5. jquery图片切换插件jquery.cycle.js参数详解

    转自:国人的力量 blog.163.com/xz551@126/blog/static/821257972012101541835491/ 自从使用了jquery.cycle.js,我觉得再也不用自己 ...

  6. ActiveXObject对象详解

    一.什么是 ActiveX 控件?         ActiveX 控件广泛用于 Internet.它们可以通过提供视频.动画内容等来增加浏览的乐趣.不过,这些程序可能出问题或者向您提供不需要的内容. ...

  7. 20160217.CCPP体系详解(0027天)

    程序片段(01):TestCmd.c 内容概要:管道_字符串 #define _CRT_SECURE_NO_WARNINGS #include <stdio.h> #include < ...

  8. 机器学习——KMeans聚类,KMeans原理,参数详解

    0.聚类 聚类就是对大量的未知标注的数据集,按数据的内在相似性将数据集划分为多个类别,使类别内的数据相似度较大而类别间的数据相似度较小,聚类属于无监督的学习方法. 1.内在相似性的度量 聚类是根据数据 ...

  9. 强化学习Q-Learning算法详解

    python风控评分卡建模和风控常识(博客主亲自录制视频教程) https://study.163.com/course/introduction.htm?courseId=1005214003&am ...

  10. matlab-霍夫变换详解(判断正方形长方形)

    霍夫变换 霍夫变换是1972年提出来的,最开始就是用来在图像中过检测直线,后来扩展能检测圆.曲线等. 直线的霍夫变换就是 把xy空间的直线 换成成 另一空间的点.就是直线和点的互换. 我们在初中数学中 ...

随机推荐

  1. JZOJ 3447.摘取作物

    \(\text{Problem}\) 在一个矩阵里选数,每行最多选两个,每列最多选两个,最大会价值 \(n,m \le 30\) \(\text{Analysis}\) 对个这个限制如何实现? 跑费用 ...

  2. 免杀之:MSF后门metasploit-loader免杀

    免杀之:MSF后门metasploit-loader免杀 目录 免杀之:MSF后门metasploit-loader免杀 1 metasploit-loader后门代码 2 在kali中编译metas ...

  3. (原创)【B4A】一步一步入门05:控件、公有属性、水平锚定、垂直锚定(控件篇01)

    一.前言 前面的教程,已经完整讲述了用B4A开发安卓APP从新建项目到编译发布的完整流程.从本篇开始,我们将会从B4A的细节处着手,一步一步掌握B4A. 从本篇开始的子系列为"控件篇&quo ...

  4. GIS若干相关的概念

    投影 常用的投影坐标系: CGCS2000_3_Degree_GK_CM_111E CGCS2000_3_Degree_GK_Zone_37 CGCS2000_GK_Zone_19 Beijing_1 ...

  5. 泛型stringToNumber

    C++中将string类型转换为double的方法:#include <iostream>#include <sstream> //使用stringstream需要引入这个头文 ...

  6. php 中解析xml文件

        public function xmltoarr($path) {//xml字符串转数组         $xml= $path;//XML文件         $objectxml = si ...

  7. DOM06~

    Window 对象 BOM (浏览器对象模型) 定时器-延时函数 js 执行机制 location 对象 navigator 对象 history 对象 BOM BOM (Browser Object ...

  8. 第三章-标准SQL语句

    3.1 SQL概述: SQL:结构化查询语言,是关系数据库的标准语言,SQL是一个通用的.功能极强的关系数据库语言 结构化查询:理解:就是只要告诉数据库我要干什么,怎么干就可以了 3.1.2 SQL的 ...

  9. Qt控制台输出显示乱码

    Qt控制台输出显示乱码 https://jingyan.baidu.com/article/ab69b2709aab0b2ca7189f3d.html

  10. HTTP知识点

    HTTP 请求/响应的步骤:(工作原理) 客户端连接到 Web 服务器 一个 HTTP 客户端,通常是浏览器,与 Web 服务器的 HTTP 端口(默认为 80)建立一个 TCP 套接字连接.例如,h ...