Bỏ qua điều hướng, tới nội dung chính
Học C
Bài 20.226 phút đọc

Khai báo và định nghĩa

Sau bài này bạn sẽ làm được

  • Phân biệt khai báo với định nghĩa cho biến, hàm và kiểu
  • Phát biểu quy tắc một định nghĩa và hậu quả khi vi phạm
  • Dùng khai báo trước để chỉ cần biết kiểu tồn tại
  • Đọc và hiểu lỗi multiple definition của trình liên kết

Hai từ này nghe như đồng nghĩa trong tiếng Việt lẫn tiếng Anh, và trong C chúng là hai chuyện hoàn toàn khác nhau. Không phân biệt được chúng thì mọi lỗi của trình liên kết đều là bí ẩn.

#Hai khái niệm, một cú pháp

Khai báo
Nói với trình biên dịch rằng một cái tên tồn tại và nó có kiểu gì. Không cấp phát gì, không sinh mã gì. Lặp lại bao nhiêu lần cũng được.
Định nghĩa
Tạo ra thứ đó thật sự: cấp chỗ trong bộ nhớ cho biến, sinh mã máy cho hàm, quy định bố cục cho kiểu. Chỉ được đúng một lần trong cả chương trình.
Dòng mãLà gìGhi chú
extern int g_dem;Khai báoCó extern, không có giá trị đầu
int g_dem;Định nghĩa tạm thờiTrường hợp đặc biệt, xem Bài 20.3
int g_dem = 0;Định nghĩaCó giá trị đầu
int cong(int, int);Khai báoChỉ có nguyên mẫu
int cong(int a, int b) { return a + b; }Định nghĩaCó thân hàm
struct S;Khai báoKhai báo trước, chưa biết bên trong
struct S { int x; };Định nghĩaĐã biết bố cục
typedef struct S S;Khai báo tên kiểuKhông định nghĩa struct S
/* Khai bao: lap lai duoc bao nhieu lan cung khong sao */
extern int g_dem;
extern int g_dem;
extern int g_dem;

int cong(int, int);
int cong(int, int);

/* Dinh nghia: chi mot lan */
int g_dem = 0;
int cong(int a, int b) { return a + b; }
terminal
gcc -std=c11 -Wall -Wextra -fsyntax-only lap-lai.c
(khong co dong nao)

#Quy tắc một định nghĩa

Quy tắc một định nghĩa
Mỗi hàm và mỗi biến có liên kết ngoài chỉ được định nghĩa đúng một lần trong toàn bộ chương trình, kể cả mọi thư viện được liên kết vào.
Ví dụ vi phạm
/* cau-hinh.h */
#ifndef CAU_HINH_H
#define CAU_HINH_H

int g_muc_log = 1;        /* DINH NGHIA trong header: SAI */

#endif

/* x1.c */
#include "cau-hinh.h"
int f1(void) { return g_muc_log; }

/* x2.c */
#include "cau-hinh.h"
int main(void) { return f1() + g_muc_log; }
terminal
gcc -std=c11 -o xa x1.c x2.c
x2.o:x2.c:(.data+0x0): multiple definition of `g_muc_log'
x1.o:x1.c:(.data+0x0): first defined here
collect2: error: ld returned 1 exit status
Định nghĩa trong header
/* cau-hinh.h */
int g_muc_log = 1;
Khai báo trong .h, định nghĩa trong .c
/* cau-hinh.h */
extern int g_muc_log;         /* KHAI BAO, lap lai duoc */

/* cau-hinh.c */
#include "cau-hinh.h"
int g_muc_log = 1;            /* DINH NGHIA, dung mot lan */
terminal
gcc -std=c11 -fno-common -o yc y1.c y2.c && ./yc; echo "exit=$?"
exit=0

#Khai báo trước

struct SinhVien;                       /* khai bao truoc */

int  db_luu(const struct SinhVien *sv);   /* dung duoc: chi can con tro */
void db_in(struct SinhVien sv);           /* LOI: can biet kich thuoc */

struct SinhVien sv;                       /* LOI, gcc bao:
                                             error: storage size of 'sv'
                                                    isn't known */
Khai báo trước
Nói rằng một kiểu tồn tại mà không nói bên trong nó có gì. Đủ để khai báo con trỏ tới nó, không đủ để tạo biến hay truy cập trường.
Việc muốn làmChỉ khai báo trước có đủ khôngVì sao
Khai báo con trỏ: struct S *p;ĐủCon trỏ luôn cùng kích thước
Tham số hàm là con trỏĐủNhư trên
Giá trị trả về là con trỏĐủNhư trên
Tạo biến: struct S s;Không đủCần biết cấp bao nhiêu byte
Truy cập trường: p->xKhông đủCần biết x ở độ lệch nào
Dùng sizeof(struct S)Không đủCần biết kích thước
Truyền theo giá trịKhông đủCần biết kích thước để sao chép
Ứng dụng: bớt include trong header
/* Cach 1: include */
/* luutru.h */
#include "sinhvien.h"                       /* keo theo ca sinhvien.h */

MaLoi lt_ghi(const char *ten, const SinhVien *ds, size_t n);

/* Cach 2: khai bao truoc */
/* luutru.h */
typedef struct SinhVien SinhVien;           /* khong keo theo gi */

MaLoi lt_ghi(const char *ten, const SinhVien *ds, size_t n);

/* luutru.c thi VAN phai include sinhvien.h, vi cai dat
   can truy cap truong. Nhung moi tep .c khac chi include
   luutru.h thi khong phai phan tich sinhvien.h nua. */

#Đọc lỗi của trình liên kết

Thông báoNghĩa làCách sửa
undefined reference to `f'Có khai báo nhưng không tìm thấy định nghĩa ở đâuThêm tệp .c chứa f vào lệnh liên kết, hoặc viết thân hàm
multiple definition of `g'Tìm thấy hơn một định nghĩaChuyển định nghĩa ra khỏi header, hoặc thêm static
conflicting types for `f'Hai khai báo cùng tên khác kiểuĐây là lỗi biên dịch, không phải liên kết. Sửa nguyên mẫu
undefined reference to `main'Không có hàm main trong số các tệp liên kếtQuên thêm main.c vào lệnh
terminal
# Quên thêm sinhvien.c vào lệnh liên kết
gcc -Iinclude src/main.c src/quanly.c -o app
/usr/bin/ld: quanly.o: in function `ql_them':
quanly.c:(.text+0x2a): undefined reference to `sv_kiem_tat_ca'
collect2: error: ld returned 1 exit status
# Thêm vào là hết
gcc -Iinclude src/main.c src/quanly.c src/sinhvien.c -o app
(khong co dong nao)

Bảng phân biệt lỗi biên dịch với lỗi liên kết

Dấu hiệuGiai đoạnNhìn vào đâu
Thông báo có tên tệp và số dòngBiên dịchĐúng dòng đó trong mã nguồn
Thông báo nhắc tới tệp .oLiên kếtDanh sách tệp trong lệnh gcc
Thông báo bắt đầu bằng ld hoặc collect2Liên kếtNhư trên
Có chữ error: kèm mã nguồn được trích raBiên dịchCú pháp và kiểu
undefined reference, multiple definitionLiên kếtKý hiệu, xem Bài 20.3

Tự làm thử

  1. Viết một tệp có ba dòng extern int g; giống hệt nhau và xác nhận nó biên dịch sạch.
  2. Đặt int g = 1; trong header, include từ hai tệp .c, rồi đọc thông báo lỗi và chỉ ra nó tới từ giai đoạn nào.
  3. Sửa lại bằng extern và xác nhận chương trình chạy.
  4. Viết struct S; rồi thử khai báo struct S s; và struct S *p;, xem cái nào lỗi.
  5. Cố ý quên một tệp .c trong lệnh liên kết và tập đọc thông báo undefined reference.
  6. Viết chương trình dùng sqrt rồi thử cả gcc -lm a.c và gcc a.c -lm.

Trình chấm điểm tự động sẽ được bổ sung ở giai đoạn sau. Hiện tại bạn tự chạy thử trên máy.

Tóm tắt

  • Khai báo nói một cái tên tồn tại và lặp lại được. Định nghĩa tạo ra nó thật sự và chỉ được một lần.
  • Chính vì khai báo lặp lại được mà một header include được từ hai mươi tệp .c.
  • Định nghĩa kiểu, hàm static và macro là ba thứ được phép lặp lại, nên chúng đặt được trong header.
  • Khai báo trước đủ cho con trỏ, không đủ cho biến, cho truy cập trường, hay cho sizeof.
  • Thông báo nhắc tệp .o là lỗi liên kết, nên hãy nhìn vào lệnh build chứ không phải mã nguồn.