Kỹ Thuật Lập Trình Hàm và Con Trỏ trong C++ (CO1011) - Đại Học Bách Khoa

Khám phá 05 kỹ thuật lập trình quan trọng với hàm và con trỏ. Nâng cao kỹ năng lập trình, tối ưu hóa code hiệu quả. Tìm hiểu ngay!

Trường đại học

Hochiminh University Of Technology

Người đăng

Ẩn danh

Thể loại

Bài giảng
77
1
0

Phí lưu trữ

30 Point

Tóm tắt

I. Tổng Quan Hàm và Con Trỏ Nền Tảng Lập Trình C Hiện Đại

Trong lập trình C++, hàm (function) và con trỏ (pointer) là hai khái niệm nền tảng, tạo nên sức mạnh và sự linh hoạt của ngôn ngữ. Việc nắm vững cách chúng hoạt động không chỉ là yêu cầu cơ bản mà còn là chìa khóa để viết mã hiệu quả, tối ưu và dễ bảo trì. Một hàm là một khối lệnh được đặt tên, có thể được gọi từ nhiều nơi trong chương trình để thực hiện một tác vụ cụ thể. Theo tài liệu giảng dạy của Đại học Bách Khoa TP.HCM, việc chia nhỏ chương trình thành các hàm giúp tránh viết mã nguyên khối (monolithic code), vốn rất khó để sửa lỗi, mở rộng và tái sử dụng. Mỗi hàm có thể nhận các tham số đầu vào và trả về một giá trị, tương tự như các hàm toán học nhưng linh hoạt hơn nhiều. Trong khi đó, con trỏ là một biến đặc biệt, giá trị của nó không phải là dữ liệu thông thường mà là địa chỉ bộ nhớ của một biến khác. Điều này cho phép thao tác trực tiếp với bộ nhớ, một khả năng mạnh mẽ để quản lý tài nguyên, đặc biệt là trong các tác vụ yêu cầu cấp phát bộ nhớ động. Sự kết hợp giữa hàm và con trỏ mở ra nhiều kỹ thuật lập trình cao cấp, chẳng hạn như truyền đối số lớn mà không cần sao chép, xây dựng các cấu trúc dữ liệu phức tạp như danh sách liên kết, cây, và triển khai các cơ chế gọi lại (callback). Hiểu rõ mối liên hệ này là bước đệm quan trọng để làm chủ C++ và giải quyết các bài toán phức tạp trong kỹ thuật phần mềm.

1.1. Định nghĩa và cú pháp khai báo hàm cơ bản trong C

Một hàm trong C++ được định nghĩa là một nhóm các câu lệnh được đặt tên để thực hiện một nhiệm vụ cụ thể. Việc sử dụng hàm giúp cấu trúc hóa chương trình, tăng khả năng tái sử dụng mã và đơn giản hóa quá trình gỡ lỗi. Cú pháp cơ bản để khai báo và định nghĩa một hàm bao gồm: kiểu trả về, tên hàm, và danh sách tham số. Ví dụ: int add(int a, int b);. Ở đây, int là kiểu dữ liệu mà hàm sẽ trả về, add là tên hàm, và (int a, int b) là danh sách các tham số đầu vào. Một hàm có thể không trả về giá trị nào, khi đó kiểu trả về sẽ là void. Trong C++, các hàm có thể có cùng tên nhưng khác nhau về danh sách tham số, một tính năng được gọi là nạp chồng hàm (function overloading). Điều này mang lại sự tiện lợi cho lập trình viên, cho phép thực hiện cùng một logic trên các kiểu dữ liệu khác nhau mà không cần đặt tên hàm khác nhau.

1.2. Giới thiệu về con trỏ và vai trò của địa chỉ bộ nhớ

Con trỏ là một trong những tính năng mạnh mẽ nhất nhưng cũng dễ gây nhầm lẫn nhất trong C++. Về bản chất, con trỏ là một biến có giá trị là địa chỉ bộ nhớ của một biến khác. Thay vì lưu trữ dữ liệu trực tiếp, nó "trỏ" đến nơi dữ liệu được lưu trữ. Để lấy địa chỉ của một biến, ta sử dụng toán tử & (address-of). Để khai báo một con trỏ, ta dùng **toán tử *** (dereference) sau kiểu dữ liệu, ví dụ: int *pInt;. Con trỏ cho phép truy cập và sửa đổi gián tiếp giá trị của biến mà nó trỏ tới, cũng như thực hiện các thao tác cấp phát bộ nhớ động trên vùng nhớ Heap. Điều này cực kỳ hữu ích khi làm việc với các mảng có kích thước thay đổi, các cấu trúc dữ liệu phức tạp hoặc khi cần truyền các đối tượng lớn vào hàm mà không muốn tốn chi phí sao chép toàn bộ dữ liệu.

II. Thách Thức Quản Lý Bộ Nhớ C Rủi Ro Từ Hàm Con Trỏ

Mặc dù hàm và con trỏ mang lại sức mạnh to lớn, chúng cũng là nguồn gốc của nhiều lỗi nghiêm trọng nếu không được sử dụng cẩn thận. Thách thức lớn nhất nằm ở việc quản lý bộ nhớ C++ thủ công. Lập trình viên phải chịu trách nhiệm hoàn toàn trong việc cấp phát và giải phóng bộ nhớ. Một sai lầm nhỏ có thể dẫn đến các lỗi khó phát hiện như rò rỉ bộ nhớ (memory leak), truy cập vùng nhớ không hợp lệ (segmentation fault), hoặc ghi đè dữ liệu quan trọng. Tài liệu nghiên cứu đã chỉ ra các lỗi phổ biến như trả về con trỏ trỏ đến biến cục bộ của hàm. Biến cục bộ tồn tại trên stack và sẽ bị hủy ngay khi hàm kết thúc, khiến con trỏ trở thành con trỏ lơ lửng (dangling pointer). Một vấn đề khác là sự nhầm lẫn giữa truyền tham trịtruyền tham chiếu. Khi truyền tham trị, một bản sao của đối số được tạo ra, mọi thay đổi trong hàm không ảnh hưởng đến biến gốc. Ngược lại, truyền tham chiếu (thực chất được trình biên dịch chuyển thành con trỏ) cho phép hàm thay đổi trực tiếp biến gốc, có thể gây ra các hiệu ứng phụ không mong muốn nếu không được kiểm soát. Hiểu rõ cách bộ nhớ được tổ chức thành các vùng như Code Segment, Data Segment, Heap và Stack là cực kỳ quan trọng để chẩn đoán và ngăn chặn các lỗi này.

2.1. Lỗi tràn bộ đệm Stack Overflow và tham chiếu không hợp lệ

Tràn bộ đệm stack (Stack Overflow) là một lỗi kinh điển xảy ra khi chương trình sử dụng quá nhiều bộ nhớ trên stack. Nguyên nhân phổ biến nhất là do các lời gọi hàm đệ quy vô hạn hoặc quá sâu. Mỗi lần một hàm được gọi, một khung stack (stack frame) chứa các biến cục bộ, tham số và địa chỉ trả về sẽ được đẩy vào stack. Nếu đệ quy không có điểm dừng, stack sẽ nhanh chóng bị lấp đầy và gây sập chương trình. Ví dụ minh họa trong tài liệu int foo(int n) { return n + foo(n + 1); } cho thấy một hàm đệ quy không có điều kiện dừng, chắc chắn sẽ dẫn đến stack overflow. Một lỗi nguy hiểm khác là trả về tham chiếu hoặc con trỏ đến một biến cục bộ. Ví dụ float* foo() { float a; return &a; }. Biến a sẽ bị hủy khi hàm foo kết thúc, con trỏ trả về sẽ trỏ tới một vùng nhớ không còn hợp lệ, gây ra hành vi không xác định khi truy cập.

2.2. Nhầm lẫn giữa truyền tham chiếu và truyền tham trị

Sự khác biệt giữa truyền tham trị (pass-by-value) và truyền tham chiếu (pass-by-reference) là một khái niệm cốt lõi trong C++. Khi một biến được truyền bằng giá trị, một bản sao của nó được tạo ra và truyền vào hàm. Mọi thay đổi trên bản sao này không ảnh hưởng đến biến gốc. Đây là cơ chế mặc định và an toàn nhất. Ngược lại, khi truyền bằng tham chiếu (sử dụng &), hàm sẽ làm việc trực tiếp trên biến gốc thông qua một bí danh (alias). Điều này hiệu quả hơn cho các đối tượng lớn vì không cần sao chép, nhưng cũng tiềm ẩn rủi ro vì hàm có thể thay đổi trạng thái của chương trình một cách không tường minh. Việc nhầm lẫn giữa hai cơ chế này có thể dẫn đến lỗi logic khó tìm, khi lập trình viên mong đợi biến gốc không đổi nhưng thực tế nó lại bị thay đổi bởi hàm.

III. Phương Pháp Tối Ưu Hàm Truyền Tham Số và Giá Trị Trả Về

Để khai thác tối đa sức mạnh của hàm và giảm thiểu rủi ro, việc áp dụng các phương pháp tối ưu là rất cần thiết. Một trong những quyết định quan trọng nhất là lựa chọn cách truyền tham số. Đối với các kiểu dữ liệu nguyên thủy (int, float, char) hoặc các đối tượng nhỏ, truyền tham trị là lựa chọn đơn giản và an toàn. Tuy nhiên, với các đối tượng lớn hoặc cấu trúc phức tạp, việc sao chép sẽ tốn kém tài nguyên. Trong trường hợp này, truyền tham chiếu (&) hoặc truyền con trỏ (*) là giải pháp hiệu quả hơn. Truyền tham chiếu hằng (const &) là một kỹ thuật tuyệt vời, cho phép tránh việc sao chép tốn kém mà vẫn đảm bảo hàm không thể sửa đổi đối số gốc, kết hợp được ưu điểm của cả hai phương pháp. Một kỹ thuật khác cần lưu ý là cách xử lý giá trị trả về. Việc trả về một con trỏ từ hàm đòi hỏi sự cẩn trọng cao độ. Chỉ nên trả về con trỏ trỏ đến bộ nhớ được cấp phát bộ nhớ động trên heap hoặc bộ nhớ tĩnh/toàn cục. Tuyệt đối không trả về địa chỉ bộ nhớ của một biến cục bộ, vì nó sẽ không còn tồn tại sau khi hàm kết thúc. Sử dụng hàm trả về con trỏ một cách an toàn là kỹ năng quan trọng để xây dựng các hàm tạo (factory functions) hoặc các hàm làm việc với tài nguyên động.

3.1. Kỹ thuật sử dụng hàm trả về con trỏ một cách an toàn

Một hàm trả về con trỏ có thể rất hữu ích, đặc biệt khi làm việc với bộ nhớ động. Tuy nhiên, nó đòi hỏi sự tuân thủ nghiêm ngặt các quy tắc an toàn. Quy tắc vàng là con trỏ được trả về phải trỏ đến một vùng nhớ vẫn hợp lệ sau khi hàm kết thúc. Điều này có nghĩa là vùng nhớ đó phải được cấp phát trên heap (sử dụng new), hoặc là một biến static, hoặc một biến toàn cục. Ví dụ, một hàm tạo đối tượng và trả về con trỏ đến nó: MyObject* createObject() { return new MyObject(); }. Người gọi hàm này sau đó có trách nhiệm giải phóng bộ nhớ bằng delete. Việc trả về địa chỉ của biến cục bộ là một lỗi nghiêm trọng vì vùng nhớ đó trên stack sẽ được tái sử dụng cho các lời gọi hàm khác, dẫn đến dữ liệu bị hỏng và hành vi không thể đoán trước.

3.2. So sánh tham số con trỏ và tham số tham chiếu

Cả tham số con trỏ và tham số tham chiếu đều cho phép hàm sửa đổi biến gốc, nhưng chúng có sự khác biệt tinh tế về cú pháp và ngữ nghĩa. Tham số tham chiếu (float &b) hoạt động như một bí danh của biến gốc. Cú pháp sử dụng trong thân hàm giống hệt như với biến thông thường. Tham chiếu phải được khởi tạo ngay khi khai báo và không thể được gán lại để tham chiếu đến một biến khác. Ngược lại, tham số con trỏ (float *p) lưu trữ địa chỉ bộ nhớ. Để truy cập giá trị, cần sử dụng toán tử dereference (*p). Con trỏ có thể được gán lại để trỏ đến các địa chỉ khác nhau và có thể có giá trị nullptr, biểu thị rằng nó không trỏ đến đâu cả. Lựa chọn giữa chúng thường phụ thuộc vào ngữ cảnh: dùng tham chiếu khi luôn cần một đối tượng hợp lệ và không muốn cú pháp con trỏ phức tạp; dùng con trỏ khi cần khả năng "trống" (nullptr) hoặc cần thay đổi đối tượng mà con trỏ trỏ tới.

IV. Bí Quyết Làm Chủ Con Trỏ Toán Tử và Trong Lập Trình C

Làm chủ con trỏ là một cột mốc quan trọng đối với bất kỳ lập trình viên C++ nào. Chìa khóa nằm ở việc hiểu sâu sắc hai toán tử cơ bản: toán tử lấy địa chỉ & và toán tử truy cập nội dung *. Toán tử & được sử dụng để lấy địa chỉ bộ nhớ của một biến. Khi áp dụng cho một biến, ví dụ &myVar, nó trả về một giá trị địa chỉ mà sau đó có thể được gán cho một biến con trỏ. Ngược lại, **toán tử ***, khi được sử dụng với một biến con trỏ (ví dụ *pVar), thực hiện hành động "dereference" – tức là truy cập vào giá trị được lưu tại địa chỉ mà con trỏ đang trỏ tới. Hai toán tử này hoạt động đối nghịch nhau. Hiểu rõ mối quan hệ giữa mảng và con trỏ cũng là một bí quyết. Trong C++, tên của một mảng thực chất hoạt động như một con trỏ hằng, trỏ đến phần tử đầu tiên của mảng. Điều này lý giải tại sao có thể sử dụng cú pháp con trỏ để truy cập các phần tử mảng (ví dụ *(arr + i)) và ngược lại (p[i]). Tuy nhiên, cần lưu ý rằng con trỏ là biến có thể thay đổi giá trị (trỏ đến nơi khác), trong khi tên mảng thì không. Nắm vững các thao tác này là nền tảng để triển khai các cấu trúc dữ liệu động và thuật toán hiệu quả.

4.1. Mối quan hệ mật thiết giữa mảng và con trỏ trong C

Trong C++, mảng và con trỏ có một mối liên hệ chặt chẽ. Tên của một mảng, khi được sử dụng trong một biểu thức, sẽ tự động phân rã (decay) thành một con trỏ trỏ đến phần tử đầu tiên của nó. Ví dụ, nếu có một mảng int arr[10], thì biểu thức arr tương đương với &arr[0]. Điều này cho phép truyền mảng vào hàm một cách hiệu quả chỉ bằng cách truyền con trỏ. Nó cũng giải thích tại sao cả hai cú pháp arr[i]*(arr + i) đều hợp lệ để truy cập phần tử thứ i của mảng. Sự tương đồng này cho phép thực hiện các phép toán số học trên con trỏ (pointer arithmetic) để duyệt qua các phần tử của mảng, một kỹ thuật rất phổ biến và hiệu quả trong lập trình cấp thấp.

4.2. Khám phá con trỏ void và kỹ thuật ép kiểu con trỏ

Một con trỏ void (void*) là một loại con trỏ đặc biệt có thể trỏ đến đối tượng của bất kỳ kiểu dữ liệu nào. Nó được xem là con trỏ "tổng quát". Vì trình biên dịch không biết kiểu dữ liệu của đối tượng mà nó trỏ tới, ta không thể trực tiếp dereference một con trỏ void. Để sử dụng, nó phải được ép kiểu (cast) một cách tường minh sang một kiểu con trỏ cụ thể khác (ví dụ int*, char*). Con trỏ void rất hữu ích trong các tình huống cần viết các hàm chung chung, có khả năng làm việc với nhiều loại dữ liệu khác nhau, chẳng hạn như trong các hàm cấp phát bộ nhớ như malloc hoặc các hàm sắp xếp như qsort từ thư viện C chuẩn. Tuy nhiên, việc ép kiểu cần được thực hiện cẩn thận để tránh các lỗi liên quan đến kiểu dữ liệu không khớp.

V. Ứng Dụng Nâng Cao Kỹ Thuật Dùng Con Trỏ Hàm C Callback

Khi đã nắm vững kiến thức cơ bản, sự kết hợp giữa hàm và con trỏ mở ra các kỹ thuật lập trình nâng cao và mạnh mẽ. Một trong những ứng dụng tiêu biểu nhất là con trỏ hàm C++ (function pointer). Tương tự như con trỏ dữ liệu trỏ đến địa chỉ của biến, con trỏ hàm trỏ đến địa chỉ của một hàm trong bộ nhớ. Điều này cho phép truyền các hàm như những tham số cho các hàm khác, lưu trữ chúng trong các cấu trúc dữ liệu, và gọi chúng một cách linh hoạt tại thời điểm chạy. Kỹ thuật này là nền tảng của cơ chế callback function C++. Một hàm callback là một hàm được truyền cho một hàm khác để được "gọi lại" sau khi một sự kiện nào đó xảy ra. Đây là mô hình phổ biến trong lập trình hướng sự kiện, giao diện người dùng, và các thư viện bất đồng bộ. Ví dụ, một hàm sắp xếp tổng quát có thể nhận một con trỏ hàm C++ làm tham số để quyết định tiêu chí so sánh giữa hai phần tử. Trong C++ hiện đại, các khái niệm này đã được phát triển thêm với std::function và lambda expressions, cung cấp một cách an toàn và linh hoạt hơn để xử lý các đối tượng có thể gọi được. Bên cạnh đó, con trỏ thông minh (smart pointers) như std::unique_ptrstd::shared_ptr đã ra đời để tự động hóa việc quản lý bộ nhớ C++, giảm thiểu đáng kể nguy cơ rò rỉ bộ nhớ.

5.1. Triển khai callback function C với con trỏ hàm

Một callback function C++ là một hàm được đăng ký để được thực thi khi một sự kiện cụ thể hoàn tất. Kỹ thuật này được triển khai hiệu quả bằng cách sử dụng con trỏ hàm C++. Cú pháp khai báo một con trỏ hàm bao gồm kiểu trả về, tên con trỏ đặt trong dấu ngoặc đơn với dấu * phía trước, và danh sách tham số của hàm mà nó có thể trỏ tới. Ví dụ: void (*myCallback)(int);. Hàm chính có thể nhận con trỏ này làm tham số, thực hiện tác vụ của mình, và sau đó gọi hàm callback thông qua con trỏ để thông báo kết quả hoặc xử lý bước tiếp theo. Mô hình này giúp tách biệt logic nghiệp vụ, làm cho mã nguồn trở nên module hóa và linh hoạt hơn, đặc biệt trong các hệ thống cần phản ứng với các sự kiện đầu vào không đồng bộ.

5.2. Chuyển đổi sang C hiện đại std function và con trỏ thông minh

Mặc dù con trỏ hàm C++ rất mạnh mẽ, cú pháp của nó có thể phức tạp và nó không thể trỏ đến các đối tượng có thể gọi được khác như lambda hay function objects. C++11 đã giới thiệu std::function, một trình bao bọc (wrapper) đa năng có thể chứa bất kỳ đối tượng nào có thể gọi được (callable object). Nó cung cấp một giao diện nhất quán, an toàn về kiểu và dễ sử dụng hơn con trỏ hàm truyền thống. Song song đó, để giải quyết các vấn đề quản lý bộ nhớ C++, con trỏ thông minh (std::unique_ptr, std::shared_ptr, std::weak_ptr) đã được giới thiệu. Chúng tự động quản lý vòng đời của đối tượng được cấp phát động, tự động giải phóng bộ nhớ khi con trỏ ra khỏi phạm vi, qua đó loại bỏ hầu hết các lỗi liên quan đến rò rỉ bộ nhớ và con trỏ lơ lửng, giúp mã nguồn an toàn và hiện đại hơn.

28/09/2025

Trích đoạn nội dung tài liệu

Hochiminh University of Technology Computer Science and Engineering - [CO1011] Fundamentals of Function and Pointer C++ Programming Lecturer: Duc Dung Nguyen Credits: 4 Outcomes ❖ Solving problems with functions ❖ Understand recursive algorithms ❖ Declare and implement recursive functions ❖ Declare and using pointers 2 Outline ❖ Function: definition, declaration, parameters, returned value ❖ Scope of variables ❖ Storage ❖ Pointer ❖ Recursion 3 Function Function ❖ You should never write monolithic code ❖ Difficult to write correctly. ❖ Difficult to debug. ❖ Difficult to extend. ❖ Hard to maintenance ❖ Non-reusable ❖ Nonsense! 5 Function ❖ Math vs.

Computer Software ❖ A function can return no value ❖ A function can take many different types of parameters ❖ A function can set as many output as it needs Input Function Output 6 Function ❖ Definition: a group of statements that is given a name, and which can be called from some point of the program. ❖ Syntax: ❖ <type> <name>(<parameters>); ❖ <type> <name>(<parameters>) { <statements> } ❖ <type>: the value returned by the function ❖ <name>: name of function ❖ <parameters>: each parameter consists of a type followed by an identifier 7 Function ❖ <type> ❖ The function can return any type ❖ At some point, it must return a value ❖ return x; ❖ The function can return nothing (sometimes it is called procedure) ❖ No need for the return statement. ❖ return statement can be used to end the function. 8 Function ❖ Example #include <iostream> #include <math.h> int generateArrayValue(int range) { return rand() % range; } int main() { int img[12][16]; for (int i = 0; i < 12; i++) { for (int j = 0; j < 16; j++) { img[i][j] = generateArrayValue(256); } } return 0; } 9 Function ❖ Example #include <iostream> #include <math.01); } } return 0; } float add(float a, float b) { return a + b; } 10 Function ❖ Example #include <iostream> #include <math.01); } } printArray(img[2], 16);// print the third line of two dimensions array return 0; } 11 Function ❖ Name ❖ Many functions can have the same name: overloaded functions.

❖ Functions with the same name must not share the same prototype ❖ Function signature: name + parameter list ❖ Provide convenience for programmer 12 Function ❖ Example #include <iostream> #include <math.h> float add(float a, float b) { return a + b; } int add(int a, int b) { return a + b; } double add(int a, double b) { return (double)a + b; } int main() { double k = 3.2) << endl; cout << add(3, -8) << endl; cout << add(1, k) << endl; return 0; } 13 Function ❖ Parameters: there are two ways to pass parameters to a function ❖ Value: the value will be copied to local variable (parameter) of the function ❖ Reference (only in C++): the parameter is associated with passed variable ❖ User can only pass variables through a reference parameter ❖ Any change in the parameter affects the variable 14 Function ❖ Example #include <iostream> #include <math.h> float add(float a, float b) { b += 1; return a + b - 1; } float foo(int a, float &b) { b *= a; return b; } int main() { float x = 2.08) << endl; x = foo(2, y); cout << x << endl; cout << y << endl; return 0; } 15 Function ❖ main: ❖ Default return value of main: 0 - the program executed successfully ❖ stdlib.h/cstdlib: ❖ EXIT_SUCCESS: same as default return value ❖ EXIT_FAILURE: the program failed 16 Function ❖ Parameter passing: ❖ C++ allows user pass parameters by value or by reference ❖ If user pass a parameter using reference, it will be translated to pointer ❖ Unlike C++, everything in Java is pass-by-value. ❖ Think about what happens in the background. ❖ C++ allows user pass default values to parameters 17 Function ❖ Example #include <iostream> #include <math.h> float add(float a, float b = 1.0f) { return a + b; } int main() { float x = 2.08) << endl; cout << “increase x: x + 1 = ” << add(x) << endl; return 0; } 18 Function ❖ Reuse functions ❖ Define prototype in header file (.h): <type> <name>(<parameters>); ❖ Must export the function if it was build in a library. ❖ Use export/import instructions: depend on platform and language ❖ Static linked libraries vs.

Dynamic linked libraries 19 Function ❖ Why do you need function prototype? ❖ To reuse a function written in another module ❖ To solve tricky situations 20 Function ❖ inline functions ❖ Similar to function, except that the compiled code will be inserted where we call inline functions. ❖ Purpose: improve performance ❖ inline <return type> <function name>(<parameters>) { <function body> } 21 Scope of Variables Scope of Variables ❖ In C/C++, the variable is effective in the scope of declaration statement ❖ In C: all variables must be declared at the beginning of the function. No initialization in declaration. ❖ In C++: variables can be declared anywhere and take effect in the declared scope ❖ void test() { for (int i = 0; i < 5;) { i += 2; } i = 10;// error } 23 Scope of Variables ❖ Global vs.

Local variables ❖ Global variables can be accessed everywhere in the function without declaration ❖ Local variables can only be accessed inside the scope where it is declared 24 Scope of Variables ❖ Global vs. Local variables #include <iostream> #include <math.h> float defaultFactor; float mul(float a, float b, bool useGlobal = false) { return useGlobal? a * defaultFactor: a * b; } int main() { defaultFactor = 2.0f; cout << “Use default factor: ” << mul(3.14159, 0, true) << endl; cout << “Multiply pi by 5: ” << mul(3.0f) << endl; return 0; } 25 Scope of Variables ❖ Global vs. Local variables ❖ Why don’t we declared everything at global scope? ❖ Benefit of local variable? ❖ When should we use global variables? ❖ When should we use local variables? 26 Scope of Variables ❖ Global vs. Local variables ❖ Local variables take precedence over global variables ❖ The :: operator is called the scope resolution operator 27 Scope of Variables ❖ Global vs.

Local variables #include <iostream> #include <math.h> float accSum; float acc(float a, float accSum) { ::accSum += a; return accSum + a; } int main() { accSum = 0.14159, 0) << endl; cout << “acc(3.14159, 1) << endl; cout << “accSum: ” << accSum << endl; return 0; } 28 Storage Storage ❖ How your program is organized? ❖ What are common errors? ❖ Memory overflow ❖ Memory corruption 30 Storage high address command line arguments //// and environment variables stack heap uninitialized data (bss) initialised to zero by exec initialized data read from program file text (code segment) low address 31 Storage ❖ Code segment: contains executable code (binary code) ❖ Data segment: ❖ Initialized data: global, static, constants ❖ Uninitialized data ❖ Heap: contains allocated memory at runtime ❖ Stack: stores local variables, passed arguments, and return address 32 Storage ❖ Common errors: ❖ Use variables without initialization ❖ Memory fault ❖ Access restricted areas ❖ Overwrite meta information on memory ❖ Stack overflow 33 Storage ❖ Uninitialized variables #include <iostream> #include <math.h> float __gVal; float foo(float a, float b) { __gVal += b; return a * b + __gVal; } int main() { float x, y; x = 0.5f; cout << foo(x, y) << endl; return 0; } 34 Storage ❖ Memory fault (access freed memory) #include <iostream> #include <math.h> float* foo(float a, float b) { a += b; return &a; } int main() { float x, y; x = 0.9f; float *pRet = foo(x, y); cout << *pRet << endl; return 0; } 35 Storage ❖ Memory fault (access restricted area) #include <iostream> #include <math.h> char* getConstString() { return “This is a string”; } int main() { char* pStr = getConstString(); cout << pStr << endl; for (int i = 0; i < 10; i++) { pStr[i] = ‘-’; } cout << pStr << endl; return 0; } 36 Storage ❖ Overwrite meta information in memory (memory corruption) #include <iostream> #include <math.h> void foo(char *pStr) { char buf[10]; strcpy(buf, pStr); } int main() { char* pStr = “This string will overwrite the local buffer”; foo(pStr); return 0; } 37 Storage ❖ Blow away your stack (stack overflow) #include <iostream> #include <math.h> int foo(int n) { return n + foo(n + 1); } int main() { cout << “This code will blow away your stack\n”; foo(0); return 0; } 38 Storage ❖ How to avoid memory errors? Heap vs. Stack errors ❖ Invalid memory access ❖ Memory leaks ❖ Mismatched allocation/deallocation ❖ Missing allocation ❖ Uninitialized memory access ❖ Cross stack access 39 Pointer Pointer ❖ What is pointer? ❖ How do we access memory so far? ❖ Through variables: use identifiers ❖ What if you need something more dynamic? ❖ Allocate on demand ❖ Vary in size ❖ Flexible access mechanism 42 Pointer ❖ C++ program does not decide exact memory address of variables, the OS does. ❖ Obtain memory address of a variable ❖ Use operator & ❖ E.534f; cout << &a << endl; 43 Pointer ❖ Declare a pointer variable ❖ <type> * <identifier>; ❖ E.: int * pInt, a; ❖ Define a pointer type ❖ typedef <type>* <alias_type>; ❖ E.: typedef int* intPointer; 44 Pointer ❖ Special values ❖ Specific areas managed by OS ❖ Stack ❖ Code address ❖ Data addresses ❖ NULL == 0 (== nullptr) 45 Pointer 3.14159 unknown #include <iostream> x pX #include <math.14159 &x int main() { float x = 3.14159; float* pX; x pX pX = &x; cout << “Value of x: ” << x << endl; cout << “Address of x: ” << pX << endl; 7.5; cout << “Value of x: ” << x << endl; pX = NULL; x pX return 0; } 7.853975 0 x pX 46 Pointer ❖ Dereference operator * ❖ Used to access the memory pointed to by the pointer variable ❖ Usage: *<pointer variable> ❖ Example: ❖ int *pX = &a; *pX = 5; a = *pX - 2; 47 Pointer ❖ Casting pointer value ❖ int *p; p = 0xff63;// error, illegal statement ❖ int *p; p = reinterpret_cast<int *>(0xff63); ❖ Use pointer wisely! 48 Pointer ❖ Pointer vs. array (static) #include <iostream> #include <math.h> ❖ Pointer can be changed! int main() { int a[10]; for (int i = 0; i < 10; i++) { ❖ Access memory in the same way a[i] = 0; cout << a[i] << " "; } ❖ A static array can be considered as a cout << endl; int *pA = a; constant pointer for (int i = 0; i < 10; i++) { pA[i] = i + 1; cout << pA[i] << " "; *a = i; } cout << endl; return 0; } 49 Pointer #include <iostream> ❖ Passing array as function parameter #include <math.h> typedef struct { ❖ Passing values: define a new int data[10]; } myStruct; structure to hold array int foo(myStruct a) { int sum = 0; ❖ int foo(int a[10]) { for (int i = 0; i < 10; i++) sum += a.data[i] = i; … } cout << “Sum = ” << foo(mA) << endl; } return 0; } 50

Nội dung được bảo vệ bản quyền — Tải xuống đầy đủ